相机系统 GLFW OPENGL

发布时间 2023-12-23 11:38:51作者: Dba_sys

0. 前言

游戏或者三维软件中的相机,与现实中的相机没有什么特别大的区别。

1. 世界坐标系

世界坐标系并非是一个特殊的坐标系,因为他得到了所有其他坐标系的认可(参照)所以被称为世界坐标系。可以用来表示位置和方向, 基于X Y Z 坐标轴。

我们日常生活中,常用 前后左右(Forward Left Right Up) 来表示其他物体相对于自身的坐标。这是一个很好,很直觉的模式。

涉及到机器人学,飞机,火箭等课程,又会常用 俯仰角(点头) 偏航角(偏头) 滚转角(飞行员在一个直立圆环上转着测试)(Pitch Yaw Roll)来描述一个物体的状态。

好了,人们更愿意用某些方位和姿态的描述性词语来 描绘 这个世界。但这无论如何不是严谨的,不是数学的。这些描述性词语需要与X Y Z坐标轴进行绑定,然后可以走进数学的殿堂。

问题是没有任何强制性的规定说 X代表东方, Y代表北方, Z轴指向天空。

所以在不同的领域里,这些绑定关系是不同的,甚至 X Y Z 坐标轴的旋转顺序也是不同的,有左手系,右手系之分。

这里举出几个例子:

2. GLFW 窗口坐标系 与 坐标系变换

GLFW的窗口坐标系以窗口的左上角为坐标原点,向右侧延展为X正轴, 向下侧延展为Y正轴。

ref Introduction to the API GLFW

我们可以把这个坐标系变换为OPENGL的屏幕坐标系。窗口中心为坐标原点,向右侧延展为X正轴,向上侧延展为Y正轴。

image

	printf("mouse_button_callback \n");
    double xpos, ypos;
    glfwGetCursorPos(m_private->window, &xpos, &ypos);
    int width, height;
    glfwGetWindowSize(m_private->window, &width, &height);
    printf("content x:%f, y:%f \n", xpos, ypos);

    float x = (float)(2 * xpos / width - 1);
    float y = (float)(2 * (height - ypos) / height - 1);
    glm::vec2 pos(x, y);
    printf("unit coordinate x:%f, y:%f \n", x, y);

glfwSetMouseButtonCallback 这东西按下去调用一次,抬起来调用一次。
https://glfw-d.dpldocs.info/v1.0.1/glfw3.api.glfwSetMouseButtonCallback.html

3. 相机是什么东西

在OpenGL中,相机代表着 View 矩阵,即将世界坐标系的物体转换到相机坐标系里。

要进行这种转换,就需要在世界坐标系下描述相机坐标系。即相机坐标原点,以及三条互相垂直的坐标轴,总共四条信息。

问题是我们需要提供这么多信息吗?实际上提供3条信息就好,一个相机原点,两条垂直的轴就可以,第三条轴可以通过叉积自动算出来。

OpenGL提供了一个简单的函数来生成这个坐标系,即LookAt()。毫无疑问,他有三条参数。

Parameters
	eye	Position of the camera
	center	Position where the camera is looking at
	up	Normalized up vector, how the camera is oriented. Typically (0, 0, 1)

https://glm.g-truc.net/0.9.9/api/a00668.html#gaa64aa951a0e99136bba9008d2b59c78e

因此,我们要更新,相机的位置与姿态,就需要改动三个值,分别是相机位置,相机拍摄的位置,相机的上侧向量。这些向量定义在世界坐标系中。

事实上,当我们在观察一个物体时,我们看到物体在屏幕里左右摇晃,实际上,不是物体在动,而是相机在动。这就是相对论~。

4. 相机的平面位移(上下左右)

想象着一个相机固定在一个平面里,甚至就在我们的屏幕的黑框里,他只能在这个平面里动来动去,但是不可以旋转(相机的上侧向量会改变)。

显而易见,相机的位置和相机拍摄的位置会发生改变,这也是相机平面位移功能主要修改的两个变量。

	/* Code from Peng Yu Bin 《OpenGL Tutor》 */
 	// translate left ,right, up and down. 
    void pan(InputCtl::InputPreference const &pref, glm::vec2 delta) {
        delta *= -pref.pan_speed;

        auto front_vector = glm::normalize(lookat - eye);
        auto right_vector = glm::normalize(glm::cross(front_vector, up_vector));
        auto fixed_up_vector = glm::normalize(glm::cross(right_vector, front_vector));

        auto delta3d = delta.x * right_vector + delta.y * fixed_up_vector;

        eye += delta3d;
        lookat += delta3d;
        printf("translate left and right \n");
    }

	glm::mat4x4 view_matrix() const {
        return glm::lookAt(eye, lookat, up_vector);
    }

根据相对论,当我们以为我们把一个物体向左移动,以为自己不动,物体左动。

但是实际上,物体并没有动,是我们在动,我们相对物体向右动。

所以当我们向左滑动物体,物体位置偏移量delta = pos - lastpos为负值,实际上却是相机向右滑动,在世界坐标系中。

为什么我们需要 fixed_up_vector, 在LearnOpenGL的Camera教程里详细的展示了如何从三个信息中生成View矩阵。在初始化的时候,我们绝不保证up_vector严格的指向相机的上方,而是要和direction vector = cameraPos - cameraTarget共面,来生成 Right axis,之后再通过direction vector X Right axis来生成真正的向上的相机向量。

glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);

那么当up_vector在初始化时与direction vector共线会发生什么情况呢?答案就是生成不出正确的View矩阵,因为存在三条信息的限制,却只给出2条信息。OpenGL直接撂挑子不干了, 会黑屏。

glm::vec3 eye = {0, 0, 5};
glm::vec3 lookat = {0, 0, 0};
glm::vec3 up_vector = {0, 0, -5};
glm::lookAt(eye, lookat, up_vector);

5. 相机的聚焦点环绕(球形环绕 ArcBall Orbit)

球形环绕可以想象成相机绕着一个球面进行环绕,相机的镜头聚焦于拍摄物体。相机的成像平面(up_vector right_vector)为球面的切平面。

相机聚焦点环绕有个非常重要的事情,那就是相机的水平轴(right-axis or X-axis or 显示器屏幕的长边。)需要尽可能的保持水平,相对于世界坐标系里正常摆放的物体。

为什么呢?因为我们坐在屏幕前的头一开始是水平摆放的,并且也一直是水平摆放的。

一旦我们的虚拟相机的水平轴发生旋转,这就像我们拿手机斜着拍摄一个物体一样,映射到屏幕上会让人很不舒服。

5.1 如何保持水平轴水平 固定向上轴

一个简便的方法就是保持up_vector = {0, 1, 0}这样无论方向向量怎么看,水平轴永远是水平的。将三个自由度化为一个自由度即眼的位置,look_atorbit里不会变化。

围绕up_vector我们可以生成一个过这个向量的平面,这个平面可有无数个,但是唯一需要注意的是相机的front_vector无论怎么变化,都是在其中的某一个平面里,因此 ringht_vector始终垂直于这个平面,并且与up_vector保持垂直。也就是说不会斜过来拍摄。

很好,似乎可以正常工作,实际上它与后面讲的修正基本没什么差别。

问题来到了特殊情况,我们知道当我们固定up_vector = {0, 1, 0}时,在移动相机的front_vector时,难免会与up_vector共线,也就是从头顶往下看。这种情况OpenGL绝对撂挑子不干, 会黑屏。

有一个好消息是,只要我们开始计算front_vector, 由于计算机数值的原因,我们几乎不会算出front_vector = {0, 1, 0} ,而是会出现front_vector vec3(-0.000316, -1.000000, -0.000136)这样的情况。也就是说,基本没有可能会与up_vector共线。

但是因此也会出现另一种状况,当我们的front_vectorup_vector将要共线时,他们挨的特别近。由于叉积的特性,他们两个的位置稍有方向上的变化(0.001度的变化),那么垂直向量会有巨大的变化(180度的变化)。

image

当我们上下滑动鼠标时调用

// rotation 2: based on the mouse vertical axis
glm::mat4x4 rotation_matrixY = glm::rotate(glm::mat4x4(1), angle_Y_inc, right_vector);

相机到了与up_vector共线的小区域,且每次调用相机绕right_vector滑动距离很小angle_Y_inc: -0.00260419。而相机right_vector的正负摆动非常快,同样的步长,正负转化,导致

  • 正负摆动,导致相机左右异常跳变。直接反应到屏幕上来。
  • 正负摆动,且是同一符号步长,导致旋转抵消。相机位置不可变化。
void orbit(InputCtl::InputPreference const &pref, glm::vec2 delta, bool isDrift) {
        if (isDrift) {
            delta *= -pref.drift_speed;
            delta *= std::atan(film_height / (2 * focal_len));
        } else {
            delta *= pref.orbit_speed;
        }

        auto angle_X_inc = delta.x;
        auto angle_Y_inc = delta.y;

        // pivot choose: drift mode rotates around eye center, orbit mode rotates around target object
        auto rotation_pivot = isDrift ? eye : lookat;

        auto front_vector = glm::normalize(lookat - eye);
        std::cout<<"front_vector "<<glm::to_string(front_vector)<<std::endl;
        // new right vector (orthogonal to front, up)
        auto right_vector = glm::normalize(glm::cross(front_vector, up_vector));

        std::cout<<"right_vector "<<glm::to_string(right_vector)<<std::endl;
        // new up vector (orthogonal to right, front)
        auto new_up_vector = glm::normalize(glm::cross(right_vector, front_vector));
        std::cout<<"new_up_vector "<<glm::to_string(new_up_vector)<<std::endl;
        

        // rotation 1: based on the mouse horizontal axis
        glm::mat4x4 rotation_matrixX = glm::rotate(glm::mat4x4(1), -angle_X_inc, new_up_vector);

        //auto new_right_vector = glm::vec3(rotation_matrixX * glm::vec4(right_vector, 1));



        // rotation 2: based on the mouse vertical axis
        glm::mat4x4 rotation_matrixY = glm::rotate(glm::mat4x4(1), angle_Y_inc, right_vector);
        std::cout<<"angle_Y_inc: "<<angle_Y_inc<<std::endl;
        std::cout<<"rotation_matrixY: "<<glm::to_string(rotation_matrixY)<<std::endl;

        // translate back to the origin, rotate and translate back to the pivot location
        auto transformation = glm::translate(glm::mat4x4(1), rotation_pivot)
            * rotation_matrixY * rotation_matrixX
            * glm::translate(glm::mat4x4(1), -rotation_pivot);
        
        std::cout<<"transformation: "<<glm::to_string(transformation)<<std::endl;
        // update eye and lookat coordinates
        

        eye = glm::vec3(transformation * glm::vec4(eye, 1));
        lookat = glm::vec3(transformation * glm::vec4(lookat, 1));
        std::cout<<"Eye: "<<glm::to_string(eye)<<std::endl;
         /**
        // try to keep the camera horizontal line correct (eval right axis error)
        float right_o_up = glm::dot(right_vector, keep_up_axis);
        float right_handness = glm::dot(glm::cross(keep_up_axis, right_vector), front_vector);
        float angle_Z_err = glm::asin(right_o_up);
        angle_Z_err *= glm::atan(right_handness);
        // rotation for up: cancel out the camera horizontal line drift
        glm::mat4x4 rotation_matrixZ = glm::rotate(glm::mat4x4(1), angle_Z_err, front_vector);
        up_vector = glm::mat3x3(rotation_matrixZ) * up_vector;
        printf("orbit \n");
        */
    }

5.1.1 上方观看 跳变LOG

front_vector vec3(-0.000316, -1.000000, -0.000136)
right_vector vec3(0.395702, 0.000000, -0.918379)
new_up_vector vec3(-0.918379, 0.000344, -0.395702)
angle_Y_inc: -0.00260419
rotation_matrixY: mat4x4((0.999997, 0.002392, -0.000001, 0.000000), (-0.002392, 0.999997, -0.001030, 0.000000), (-0.000001, 0.001030, 0.999999, 0.000000), (0.000000, 0.000000, 
0.000000, 1.000000))
transformation: mat4x4((0.999997, 0.002392, -0.000001, 0.000000), (-0.002392, 0.999997, -0.001030, 0.000000), (-0.000001, 0.001030, 0.999999, 0.000000), (0.000000, 0.000000, 0.000000, 1.000000))
Eye: vec3(-0.010380, 4.999970, -0.004472)

front_vector vec3(0.002076, -0.999998, 0.000894)
right_vector vec3(-0.395702, 0.000000, 0.918379)
new_up_vector vec3(0.918377, 0.002260, 0.395701)
rotation_matrixY: mat4x4((0.999997, -0.002392, -0.000001, 0.000000), (0.002392, 0.999997, 0.001030, 0.000000), (-0.000001, -0.001030, 0.999999, 0.000000), (0.000000, 0.000000, 
0.000000, 1.000000))
transformation: mat4x4((0.999997, -0.002392, -0.000001, 0.000000), (0.002392, 0.999997, 0.001030, 0.000000), (-0.000001, -0.001030, 0.999999, 0.000000), (0.000000, 0.000000, 0.000000, 1.000000))
Eye: vec3(0.001578, 4.999983, 0.000680)

5.2 不固定向上轴 导致水平轴发生旋转

这个最后需要进行修正。

我们放开固定向上轴的限制,反而在过程中更新向上轴,同时使用上一帧的向上轴来构建新的向上轴以及其他一系列轴。

不固定向上轴带来一个问题,那就是我们的向上轴的朝向可以是任意的。拿出我们的手机进行拍摄,你随意的摆放向上轴,会发现因为水平轴也一并变得随意了。

而我们是要固定水平轴的。所以最后要进行一个奇妙的修正。

 void orbit(InputCtl::InputPreference const &pref, glm::vec2 delta, bool isDrift) {
        if (isDrift) {
            delta *= -pref.drift_speed;
            delta *= std::atan(film_height / (2 * focal_len));
        } else {
            delta *= pref.orbit_speed;
        }

        auto angle_X_inc = delta.x;
        auto angle_Y_inc = delta.y;

        // pivot choose: drift mode rotates around eye center, orbit mode rotates around target object
        auto rotation_pivot = isDrift ? eye : lookat;

        auto front_vector = glm::normalize(lookat - eye);

        // new right vector (orthogonal to front, up)
        auto right_vector = glm::normalize(glm::cross(front_vector, up_vector));

        // new up vector (orthogonal to right, front)
        up_vector = glm::normalize(glm::cross(right_vector, front_vector));

		// 这块的正负  -angle_X_inc  angle_Y_inc 最好拿自己的拳头当作相机,比划一下。
        // rotation 1: based on the mouse horizontal axis
        glm::mat4x4 rotation_matrixX = glm::rotate(glm::mat4x4(1), -angle_X_inc, up_vector);

        //auto new_right_vector = glm::vec3(rotation_matrixX * glm::vec4(right_vector, 1));



        // rotation 2: based on the mouse vertical axis
        glm::mat4x4 rotation_matrixY = glm::rotate(glm::mat4x4(1), angle_Y_inc, right_vector);

        // translate back to the origin, rotate and translate back to the pivot location
        auto transformation = glm::translate(glm::mat4x4(1), rotation_pivot)
            * rotation_matrixY * rotation_matrixX
            * glm::translate(glm::mat4x4(1), -rotation_pivot);

        // update eye and lookat coordinates
        eye = glm::vec3(transformation * glm::vec4(eye, 1));
        lookat = glm::vec3(transformation * glm::vec4(lookat, 1));

         /**
        // try to keep the camera horizontal line correct (eval right axis error)
        float right_o_up = glm::dot(right_vector, keep_up_axis);
        float right_handness = glm::dot(glm::cross(keep_up_axis, right_vector), front_vector);
        float angle_Z_err = glm::asin(right_o_up);
        angle_Z_err *= glm::atan(right_handness);
        // rotation for up: cancel out the camera horizontal line drift
        glm::mat4x4 rotation_matrixZ = glm::rotate(glm::mat4x4(1), angle_Z_err, front_vector);
        up_vector = glm::mat3x3(rotation_matrixZ) * up_vector;
        printf("orbit \n");
        */
    }