OpenGL学习:坐标变换(2)-视变换(view transformation)

来源:互联网 发布:数字摇号软件 编辑:程序博客网 时间:2024/06/05 03:43

OpenGL中的坐标处理过程包括模型变换、视变换、投影变换、视口变换等内容,这个主题的内容有些多,因此分节学习,主题将分为5节内容来学习。上一节模型变换,本节学习模型变换的下一阶段——视变换。到目前位置,主要在2D下编写程序,学习了视变换后,我们可以看到3D应用的效果了。本节示例程序均可在https://github.com/wangdingqiao/noteForOpenGL/tree/master/viewTransformation下载

通过本节可以了解到

  • 视变换的概念
  • 索引绘制立方体
  • 相机位置随时间改变的应用程序

坐标处理的全局过程

OpenGL中的坐标处理包括模型变换、视变换、投影变换、视口变换等内容,具体过程如下图1所示:

坐标变换

今天我们学习第2个阶段——视变换(view tansformation)

并不存在真正的相机

OpenGL成像采用的是虚拟相机模型。在场景中你通过模型变换,将物体放在场景中不同位置后,最终哪些部分需要成像,显示在屏幕上,主要由视变换和后面要介绍的投影变换、视口变换等决定。

其中视变换阶段,通过假想的相机来处理矩阵计算能够方便处理。对于OpenGL来说并不存在真正的相机,所谓的相机坐标空间(camera space 或者eye space)只是为了方便处理,而引入的坐标空间。

在现实生活中,我们通过移动相机来拍照,而在OpenGL中我们通过以相反方式调整物体,让物体以适当方式呈现出来。例如,初始时,相机镜头指向-z轴,要观察-z轴上的一个立方体的右侧面,那么有两种方式:

  1. 相机绕着+y轴,旋转+90度,此时相机镜头朝向立方体的右侧面,实现目的。注意这时立方体并没有转动。

  2. 相机不动,让立方体绕着+y轴,旋转-90度,此时也能实现同样的目的。注意这时相机没有转动。完成这一旋转的矩阵记作Ry(π2)

在OpenGL中,采用方式2来完成物体成像的调整。例如下面的图表示了假想的相机:

相机


进一步说明

进一步说明这里相对的概念,对这个概念不感兴趣的可以跳过。默认时相机位于(0,0,0),指向-z轴,相当于调用了:

glm::lookAt(glm::vec(0.0f,0.0f,0.0f),        glm::vec3(0.0f, 0.0f, -1.0f),        glm::vec3(0.0f, 1.0f, 0.0f)),
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

得到是单位矩阵,这是相机的默认情况。

上述第一种方式,相机绕着+y轴旋转90度,相机指向-x轴,则等价于调用变为:

   glm::mat4 view =glm::lookAt(glm::vec(0.0f,0.0f,0.0f),        glm::vec3(-1.0f, 0.0f, 0.0f),        glm::vec3(0.0f, 1.0f, 0.0f)),
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

得到的视变换矩阵为:

view=0010010010000001

上述第二种方式,通过立方体绕着+y轴旋转-90度,则得到的矩阵M,相当于:

   glm::mat4 model = glm::rotate(glm::mat4(1.0), glm::radians(-90.0f), glm::vec3(0.0, 1.0, 0.0));
  • 1
  • 1

这里得到的矩阵M和上面的矩阵view是相同的,可以自行验证下。 
也就是说,通过旋转相机+y轴90度,和旋转立方体+y轴-90度,最终计算得到的矩阵相同。调整相机来得到观察效果,可以通过相应的方式来调整物体达到相同的效果。在OpenGL中并不存在真正的相机,这只是一个虚构的概念。

OpenGL中视变换的实现

在OpenGL中,我们可以通过函数glm::lookAt来实现相机指定,这个函数计算的就是上面求出的视变换矩阵。以前glu版本实现为gluLookAt,这两个函数完成的功能是一样的,参数定义如下:

API lookAt ( GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ, 

GLdouble centerX, GLdouble centerY, GLdouble centerZ, 

GLdouble upX, GLdouble upY, GLdouble upZ) 
其中eye指定相机位置,center指定相机指向目标位置,up指定viewUp向量。

利用GLM数学库一般实现为:

glm::mat4 view = glm::lookAt(eyePos,    glm::vec3(0.0f, 0.0f, 0.0f),     glm::vec3(0.0f, 1.0f, 0.0f));
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

下面利用这个函数进行一些实验,以帮助理解。在设置相机参数之前,我们学习下绘制立方体,为实验增加素材。

绘制立方体

前面学习使用索引绘图一节使用了索引了矩形,如果利用索引绘制立方体,表面上看确实可以节省顶点数据,但是存在的问题是,不能为不同面上的共同顶点指定不同的纹理坐标,这在某些情况下会出现问题的。例如下面使用索引绘制的立方体: 
索引绘制 
由于在正面和侧面的顶点制定了相同的纹理坐标,插值后纹理一致,并没有出现可爱的猫咪图案。为此,我们需要为共用顶点指定不同的顶点属性,那么解决办法之一是,继续使用顶点数组绘制方式,定义立方体的数据如下:

  // 指定顶点属性数据 顶点位置 颜色 纹理GLfloat vertices[] = {    -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,   // A    0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,    // B    0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,     // C    0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,     // C    -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,    // D    -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,   // A    -0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,  // E    -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0, 1.0f,    // H    0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,    // G        0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,    // G    0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f,   // F    -0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,  // E    -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,    // D    -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0, 1.0f,    // H    -0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f,  // E    -0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f,  // E    -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,   // A    -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,    // D    0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f,   // F    0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,    // G    0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,     // C    0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,    // C    0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,    // B    0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f,   // F    0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,    // G    -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0, 1.0f,    // H    -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,    // D    -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,    // D    0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, // C    0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,    // G    -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,   // A    -0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f,  // E    0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,   // F    0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,   // F    0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,    // B    -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,   // A    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

着色器使用上一节绘制矩形的着色器程序。绘制立方体后,通过设定相机位置随着时间发生改变来观察这个立方体。指定相机位置为在xoz平面圆周运动的点轨迹,代码为:

GLfloat radius = 90.0f;//指定相机围绕物体转的半径,从效果来看就是设置远近GLfloat xPos = radius * cos(glfwGetTime());GLfloat zPos = radius * sin(glfwGetTime());glm::vec3 eyePos(xPos, 0.0f, zPos);glm::mat4 view = glm::lookAt(eyePos,glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

同时在代码中指定投影方式为透视投影,代码为:

// 投影矩阵glm::mat4 projection = glm::perspective(glm::radians(45.0f),        (GLfloat)(WINDOW_WIDTH) / WINDOW_HEIGHT, 1.0f, 100.0f);
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

投影方式和投影矩阵的计算将在后面小结介绍,这里只需要知道使用方法即可。

实现绘制立方体,效果如下图所示:

需要开启深度测试

从上面的图中我们看到了奇怪的现象,立方体后面的部分绘制在了前面的部分上,这种现象是由于深度测试(Depth Test)未开启影响的。深度测试根据物体在场景中到观察者的距离,根据设定的glDepthFunc函数判定是否通过深度测试,默认为GL_LESS,即深度小者通过测试绘制在最终的屏幕上。关于深度测试这个主题,后面会继续学习,这里不再展开。 
OpenGL中开启深度测试方法:

  glEnable(GL_DEPTH_TEST);
  • 1
  • 1

同时在主循环中,清除深度缓冲区和颜色缓冲区:

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  • 1
  • 1

开启深度测试后,旋转相机来观察立方体,效果如下: 
旋转的相机

viewUp向量

上面提到了在指定相机时需要指定相机的viewUP向量,这个向量指定了相机中哪个方向是向上的。对于相机而言,指定了相机位置eye和相机指向位置target后确定了相机的指向,位置不变,指向不变时,还是可以通过改变这个viewUp而影响成像的。这个类似于你眼睛的位置不变,看着的方向不变,但是你可以扭动脖子来确定哪个方向是向上,这个viewUp好比头顶给定的方向。相机位置固定在(0,0,3.0),指向原点,依次取viewUp为(0,1,0),(1,0,0),(0,1,0),绘制立方体后的效果如下图所示:

viewUp

图中viewUp为(0,1,0)时猫的尾巴朝上,为(1,0,0)时相当于把脖子右旋转90度,看到猫的尾巴是在左边的;为(0,-1,0)相当于倒立过来看,猫的尾巴是向下的。如果想了解更多关于viewUp的解释,可以参考What exactly is the UP vector in OpenGL’s LookAt function.

更多立方体

上一节介绍了模型变换,我们可以利用模型变换,在场景中绘制多个立方体,同时相机的位置可以采用圆的参数方程或者球面参数方程设定。绘制多个立方体的方法:

 // 指定立方体位移 glm::vec3 cubePostitions[] = {        glm::vec3(0.0f, 0.0f, 1.2f),        glm::vec3(0.0f, 0.0f, 0.0f),        glm::vec3(1.2f, 1.2f, 0.0f),        glm::vec3(-1.2f, 1.2f, 0.0f),        glm::vec3(-1.2f, -1.5f, 0.0f),        glm::vec3(1.2f, -1.5f, 0.0f),        glm::vec3(0.0f, 0.0f, -1.2f),    };  // 在主循环中绘制立方体for (int i = 0; i < sizeof(cubePostitions) / sizeof(cubePostitions[0]); ++i){    model = glm::mat4();    model = glm::translate(model, cubePostitions[i]);            glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),        1, GL_FALSE, glm::value_ptr(model));    glDrawArrays(GL_TRIANGLES, 0, 36);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

对相机位置随着时间进行改变,可以采用圆的参数方程或者球面参数方程设定。这里只是作为一个示例来设定,可以根据你的具体需求设定对应角度值。示例代码如下:

 // xoz平面内圆形坐标glm::vec3 getEyePosCircle(){    GLfloat radius = 6.0f;    GLfloat xPos = radius * cos(glfwGetTime());    GLfloat zPos = radius * sin(glfwGetTime());    return glm::vec3(xPos, 0.0f, zPos);}// 球形坐标 这里计算theta phi角度仅做示例演示// 可以根据需要设定glm::vec3 getEyePosSphere(){    GLfloat radius = 6.0f;    GLfloat theta = glfwGetTime(), phi = glfwGetTime() / 2.0f;    GLfloat xPos = radius * sin(theta) * cos(phi);    GLfloat yPos = radius * sin(theta) * sin(phi);    GLfloat zPos = radius * cos(theta);    return glm::vec3(xPos, yPos, zPos);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

例如利用球面坐标方程设定的相机位置,效果如下图所示:

球形相机坐标

最后的说明

经过视变换后,世界坐标系中坐标转换到了相机坐标系下。需要注意的相机在OpenGL中是个假想的概念,本质是通过矩阵来完成计算的。本节设定相机位置为圆周或者球面运动轨迹,并不能让用户来交互地观察场景中物体,下一节将设计一个第一人称FPS相机,让用户通过键盘和鼠标控制相机,更好地观察场景。
from http://blog.csdn.net/wangdingqiaoit/article/details/51570001