OpenGL基础16:视角

一、欧拉角

三种欧拉角:

  • 俯仰角(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的顺序,即

其中\begin{aligned} c_{1}=\cos (\alpha)=\cos \left(Y_{y a w}\right), s_{1}=\sin \alpha=\sin \left(Y_{y a w}\right) \\ c_{2}=\cos (\beta)=\cos \left(X_{p i t c h}\right), s_{2}=\sin \beta=\sin \left(X_{p i t c h}\right) \\ c_{3}=\cos (\gamma)=\cos \left(Z_{r o l l}\right), s_{3}=\sin \gamma=\sin \left(Z_{r o l l}\right) \end{aligned}

对于②重新计算:

很好理解,还是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上复现万向节死锁非常容易:

  1. 新建一个圆柱体,Reset一下它的位置和旋转属性
  2. 让它沿x轴旋转90°
  3. 这个时候你就会发现修改Y的度数和Z的度数都是偏航!失去了滚转的自由度

二、摄像机视角

回到OpenGL,延续下上一章:OpenGL基础15:输入控制

上一章实现了摄像机的移动和缩放,那么这一章就把最后的鼠标控制摄像机的视角也实现了吧

LookAt里面有3个属性,摄像机位置,目标位置和世界上向量,改变摄像机视角的方法正是改变这个目标位置

对于控制视角,我们需要关心摄像机的俯仰角和偏航角,并将其转换为向量

一步一步来,我们先只考虑俯仰角,如下图:

我们现在在OpenGL的ZY平面上,其中蓝色线与橙色线的夹角就是俯仰角,设蓝色边为单位长度1,那么绿色线的长度就是sin(\alpha ),橙色线的长度就是cos(\alpha ),因此我们就可以得出 Camera_{front} = (0,\, sin(\alpha ), \,cos(\alpha ))

代码添加如下:其中 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,

一样的公式,可以得出Camera_{front} = (sin(\gamma ),\, 0, \,cos(\gamma))

两者结合:

我们当然要指定顺序,一样按照Y-X的顺序来,不过这样的话,pitch操作就会被yaw的操作所影响,看下上面的两个公式:

  • Camera_{front} = (0,\, sin(\alpha ), \,cos(\alpha ))
  • Camera_{front} = (sin(\gamma ),\, 0, \,cos(\gamma))

分析后发现,这正是对应的旋转矩阵对向量(0, 0, -1)相乘得出的结果(最终正负和坐标系有关)

因此我们只需要套入上面的矩阵运算就可以得到最终结果:Camera_{front} = (sin(\gamma )cos(\alpha ),\, sin(\alpha ), \,-cos(\gamma)cos(\alpha ))

代码如下:其中 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);
}

猜你喜欢

转载自blog.csdn.net/Jaihk662/article/details/106519595