一、欧拉角
三种欧拉角:
- 俯仰角(Pitch):沿x轴旋转的角,从上往下看的角
- 偏航角(Yaw):沿y轴旋转的角,从左往右看的角
- 滚转角(Roll):沿z轴旋转的角(对于摄像机而言,一般不关心这个)
关于坐标轴:
- 自身坐标系:物体自身的坐标轴,显然如果物体进行了俯仰、偏航、滚筒的旋转操作,那么坐标轴方向也会被改变
- 世界坐标系:和物体无关,用来描述物体在世界中的位置,有唯一的原点和轴向
- 惯性坐标系:自身坐标系到世界坐标系的过渡,原点为对应物体的原点,会随物体的移动而改变,轴向和世界坐标系的轴向一致,不会因为物体的旋转操作而改变
一个值得思考的问题
我们假设一个物体的位置是(5, 6, 15),欧拉角是(50°, 30°, 70°)
- 对于物体移动,从(0, 0, 0)到(5, 6, 15),按照世界坐标/惯性坐标的轴向移动:顺序无关,也就说按照(0, 0, 0) → (5, 0, 0) → (5, 6, 0) → (5, 6, 15)的方式移动和按照(0, 0, 0) → (0, 6, 0) → (0, 6, 15) → (5, 6, 15)的方式移动不会影响物体的最终位置,
尽管这看上去像是句废话 - 对于物体移动,从(0, 0, 0)到(5, 6, 15),按照自身轴向移动:顺序无关,同上,毕竟移动并不会改变自身坐标轴的朝向
- 对于物体旋转,从(0, 0, 0)到(50°, 30°, 70°),按照世界坐标/惯性坐标的轴向旋转:顺序有关!也就是说按照(0, 0, 0) → (50°, 0, 0) → (50°, 30°, 0) → (50°, 30°, 70°)的方式旋转和按照(0, 0, 0) → (0, 30°, 0) → (50°, 30°, 0) → (50°, 30°, 70°)的方式得出来的物体状态是不同的!可以尝试拿身边的物品感受一下
- 对于物体旋转,从(0, 0, 0)到(50°, 30°, 70°),按照自身坐标系轴向旋转:顺序有关,同上,这个就很明显了,因为你每次旋转都会导致坐标轴同时发生改变
这下问题就大了,也就是如果我们单纯的说一个物体的欧拉角是(50°, 30°, 70°),那么好像并不能确定物体的状态
规定:必须要保证一个欧拉角确定唯一的状态,为了解决这个问题,那就需要确定旋转次序①!并且对于每次更新欧拉角的操作②,底层都从(0, 0, 0)重新开始计算,又或者使用四元数替代欧拉角③
对于①规定旋转顺序(rotate order):
3个轴共有6种顺序
举个例子:对于Unity3D来讲,就是y-x-z的顺序,即
其中
对于②重新计算:
很好理解,还是Unity3D,假设你已经通过rotate(50°, 0, 0)让物体绕x轴旋转了50°,那么再次rotate(50°, 30°, 0)的话,并不是在(50°, 0, 0)的基础上进行旋转,而是重新从(0, 0, 0)开始,按照Y-X-Z的顺序/规则旋转
这样在①②的情况下,每一个欧拉角就唯一确定了物体的状态
对于③四元数:
四元数是完全替代欧拉角的一种表示方法,相对于欧拉角更加复杂和困难,这一章就暂时不讲了。。
其实对于理论逻辑/底层计算来讲,都应该使用四元数而并非欧拉角,这是因为上面提出的问题虽然可以被解决,但也因此出现了一个新的问题:万向节死锁(Gimbal Lock)
万向节死锁:
网上关于万向节死锁的讨论其实真的非常多,这里当然不会重新讲一遍,当然讲了也未必有别人讲得好(这也是OpenGL的教程并非数学教程),这里只做小小的补充吧
你可能看了很多篇文章也不太能完全能理解万向节死锁,包括但不限于为什么这么算,产生的原因和造成的影响
其实上面已经包含对万向节死锁产生原因的解释了,我们为了确保欧拉角的唯一,所以采用了一系列的解决方案,也就是确定了旋转顺序,并且每次都是重新从(0, 0, 0)开始计算等,因为在Unity3D上是Y-X-Z的顺序,所以我们想在Unity3D上复现万向节死锁非常容易:
- 新建一个圆柱体,Reset一下它的位置和旋转属性
- 让它沿x轴旋转90°
- 这个时候你就会发现修改Y的度数和Z的度数都是偏航!失去了滚转的自由度
二、摄像机视角
回到OpenGL,延续下上一章:OpenGL基础15:输入控制
上一章实现了摄像机的移动和缩放,那么这一章就把最后的鼠标控制摄像机的视角也实现了吧
LookAt里面有3个属性,摄像机位置,目标位置和世界上向量,改变摄像机视角的方法正是改变这个目标位置
对于控制视角,我们需要关心摄像机的俯仰角和偏航角,并将其转换为向量
一步一步来,我们先只考虑俯仰角,如下图:
我们现在在OpenGL的ZY平面上,其中蓝色线与橙色线的夹角就是俯仰角,设蓝色边为单位长度1,那么绿色线的长度就是,橙色线的长度就是,因此我们就可以得出
代码添加如下:其中 pitch 的初始值为 0°
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
GLfloat sensitivity = 0.05;
xoffset *= sensitivity;
yoffset *= sensitivity;
pitch += yoffset;
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
glm::vec3 front;
front.x = 0;
front.y = sin(glm::radians(pitch));
front.z = -cos(glm::radians(pitch));
cameraFront = glm::normalize(front);
}
然后只考虑偏航角,还是上面的图,只不过这次所在的平面并非YZ而是XZ,
一样的公式,可以得出
两者结合:
我们当然要指定顺序,一样按照Y-X的顺序来,不过这样的话,pitch操作就会被yaw的操作所影响,看下上面的两个公式:
分析后发现,这正是对应的旋转矩阵对向量相乘得出的结果(最终正负和坐标系有关)
因此我们只需要套入上面的矩阵运算就可以得到最终结果:
代码如下:其中 yaw 和 pitch 的初始值都为 0°
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
GLfloat sensitivity = 0.05;
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
glm::vec3 front;
front.x = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = -cos(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(front);
}
最终效果:
三、摄像机类
像之前的Shader.h一样,我们将摄像机单独抽出来:
也正是上面的完整代码(Shader.h,两个着色器未改变)
Camera.h:
#ifndef CAMERA_H
#define CAMERA_H
#include<vector>
#include<opengl/glew.h>
#include<glm/glm.hpp>
#include<glm/gtc/matrix_transform.hpp>
enum Camera_Movement
{
FORWARD,
BACKWARD,
LEFT,
RIGHT
};
const GLfloat YAW = 0.0f; //y轴,偏航
const GLfloat PITCH = 0.0f; //x轴,俯仰
const GLfloat ZOOM = 45.0f; //视角,用于缩放
const GLfloat SPEED = 1.0f; //速度,用于移动
const GLfloat SENSITIVTY = 1.0f; //鼠标灵敏度
class Camera
{
public:
glm::vec3 Position;
glm::vec3 Front, Up, Right;
glm::vec3 WorldUp;
GLfloat Yaw, Pitch;
GLfloat MovementSpeed;
GLfloat MouseSensitivity;
GLfloat Zoom;
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 3.0f),
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f),
GLfloat yaw = YAW, GLfloat pitch = PITCH):
Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(SPEED), MouseSensitivity(SENSITIVTY), Zoom(ZOOM)
{
this->Position = position;
this->WorldUp = up;
this->Yaw = yaw;
this->Pitch = pitch;
this->updateCameraVectors();
}
Camera(GLfloat posX, GLfloat posY, GLfloat posZ, GLfloat upX, GLfloat upY, GLfloat upZ, GLfloat yaw, GLfloat pitch):
Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVTY), Zoom(ZOOM)
{
this->Position = glm::vec3(posX, posY, posZ);
this->WorldUp = glm::vec3(upX, upY, upZ);
this->Yaw = yaw;
this->Pitch = pitch;
this->updateCameraVectors();
}
//获取对应的LookAt矩阵
glm::mat4 GetViewMatrix()
{
//printf("%.2f, %.2f, %.2f\n", this->Position.x, this->Position.y, this->Position.z);
return glm::lookAt(this->Position, this->Position + this->Front, this->WorldUp);
}
void ProcessKeyboard(int direction, GLfloat deltaTime)
{
GLfloat velocity = this->MovementSpeed * deltaTime;
if (direction == FORWARD)
this->Position += this->Front * velocity;
if (direction == BACKWARD)
this->Position -= this->Front * velocity;
if (direction == LEFT)
this->Position -= this->Right * velocity;
if (direction == RIGHT)
this->Position += this->Right * velocity;
}
void ProcessMouseMovement(GLfloat xoffset, GLfloat yoffset, GLboolean constrainPitch = true)
{
xoffset *= this->MouseSensitivity;
yoffset *= this->MouseSensitivity;
this->Yaw += xoffset;
this->Pitch += yoffset;
if (constrainPitch)
{
if (this->Pitch > 89.0f)
this->Pitch = 89.0f;
if (this->Pitch < -89.0f)
this->Pitch = -89.0f;
}
this->updateCameraVectors();
}
void ProcessMouseScroll(GLfloat yoffset)
{
if (this->Zoom >= 1.0f && this->Zoom <= 45.0f)
this->Zoom -= yoffset;
if (this->Zoom <= 1.0f)
this->Zoom = 1.0f;
if (this->Zoom >= 45.0f)
this->Zoom = 45.0f;
}
private:
void updateCameraVectors()
{
glm::vec3 front;
front.x = sin(glm::radians(this->Yaw)) * cos(glm::radians(this->Pitch));
front.y = sin(glm::radians(this->Pitch));
front.z = -cos(glm::radians(this->Yaw)) * cos(glm::radians(this->Pitch));
this->Front = glm::normalize(front);
this->Right = glm::normalize(glm::cross(this->Front, this->WorldUp));
this->Up = glm::normalize(glm::cross(this->Right, this->Front));
}
};
#endif
main:
#include<iostream>
#include<opengl/glew.h>
#define GLEW_STATIC
#include<GLFW/glfw3.h>
#include"Camera.h"
#include<glm/glm.hpp>
#include<glm/gtc/matrix_transform.hpp>
#include<glm/gtc/type_ptr.hpp>
#include"Shader.h"
#include<opengl/freeglut.h>
#include<SOIL.h>
bool keys[1024];
Camera camera;
GLfloat lastX, lastY;
bool firstMouse = true;
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void cameraMove();
const GLuint WIDTH = 800, HEIGHT = 600;
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
glfwMakeContextCurrent(window);
glfwSetKeyCallback(window, key_callback);
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
//glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glewExperimental = GL_TRUE;
glewInit();
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
Shader shaderYellow("VShader.txt", "FShaderY.txt");
GLfloat 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
};
GLuint VBO, VAO, texture;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenTextures(1, &texture);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindTexture(GL_TEXTURE_2D, texture);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
int picWidth, picHeight;
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);
unsigned char* image = SOIL_load_image("Texture/wood.jpg", &picWidth, &picHeight, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, picWidth, picHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
SOIL_free_image_data(image);
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glEnable(GL_DEPTH_TEST);
while (!glfwWindowShouldClose(window))
{
glfwPollEvents();
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glClear(GL_DEPTH_BUFFER_BIT);
cameraMove();
glBindTexture(GL_TEXTURE_2D, texture);
shaderYellow.Use();
float radius = 5.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = glm::mat4(1.0f);
glm::mat4 projection = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(57.0f), glm::vec3(-0.5f, 1.0f, 0.0f));
view = camera.GetViewMatrix();
projection = glm::perspective(glm::radians(camera.Zoom), (GLfloat)WIDTH / (GLfloat)HEIGHT, 0.1f, 100.0f);
GLint modelLoc = glGetUniformLocation(shaderYellow.Program, "model");
GLint viewLoc = glGetUniformLocation(shaderYellow.Program, "view");
GLint projLoc = glGetUniformLocation(shaderYellow.Program, "projection");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glfwSwapBuffers(window);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
GLfloat deltaTime = 0.0f;
GLfloat lastFrame = 0.0f;
void cameraMove()
{
GLfloat currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
GLfloat cameraSpeed = 1.0f * deltaTime;
if (keys[GLFW_KEY_W])
camera.ProcessKeyboard(Camera_Movement(FORWARD), deltaTime);
if (keys[GLFW_KEY_S])
camera.ProcessKeyboard(Camera_Movement(BACKWARD), deltaTime);
if (keys[GLFW_KEY_A])
camera.ProcessKeyboard(Camera_Movement(LEFT), deltaTime);
if (keys[GLFW_KEY_D])
camera.ProcessKeyboard(Camera_Movement(RIGHT), deltaTime);
}
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
if (action == GLFW_PRESS) //如果当前是按下操作
keys[key] = true;
else if (action == GLFW_RELEASE) //松开键盘
keys[key] = false;
}
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll(yoffset);
}
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
GLfloat xoffset = xpos - lastX;
GLfloat yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
GLfloat sensitivity = 0.05;
xoffset *= sensitivity;
yoffset *= sensitivity;
camera.ProcessMouseMovement(xoffset, yoffset);
}