写在前面
OpenGL中的坐标处理过程包括模型变换、视变换、投影变换、视口变换等内容,这个主题的内容有些多,所以分节学习,主题将分为5节内容来学习。上一节模型变换,本节学习模型变换的下一阶段——视变换。到目前位置,主要在2D下编写程序,学习了视变换后,咱们能够看到3D应用的效果了。本节示例程序都可在个人github下载。css
经过本节能够了解到html
OpenGL中的坐标处理包括模型变换、视变换、投影变换、视口变换等内容,具体过程以下图1所示:git
每个过程处理都有其缘由,这些内容计划将会在不一样节里分别介绍,最后再总体把握一遍。
今天咱们学习第二个阶段——视变换。github
OpenGL成像采用的是虚拟相机模型。在场景中你经过模型变换,将物体放在场景中不一样位置后,最终哪些部分须要成像,显示在屏幕上,主要由视变换和后面要介绍的投影变换、视口变换等决定。web
其中视变换阶段,经过假想的相机来处理矩阵计算可以方便处理。对于OpenGL来讲并不存在真正的相机,所谓的相机坐标空间(camera space 或者eye space)只是为了方便处理,而引入的坐标空间。数组
在现实生活中,咱们经过移动相机来拍照,而在OpenGL中咱们经过以相反方式调整物体,让物体以适当方式呈现出来。例如,初始时,相机镜头指向-z轴,要观察-z轴上的一个立方体的右侧面,那么有两种方式:ide
相机绕着+y轴,旋转+90度,此时相机镜头朝向立方体的右侧面,实现目的。注意这时立方体并无转动。svg
相机不动,让立方体绕着+y轴,旋转-90度,此时也能实现一样的目的。注意这时相机没有转动。完成这一旋转的矩阵记做
在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)),
获得是单位矩阵,这是相机的默认状况。
上述第一种方式,相机绕着+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)),
获得的视变换矩阵为:
上述第二种方式,经过立方体绕着+y轴旋转-90度,则获得的矩阵M,至关于:
glm::mat4 model = glm::rotate(glm::mat4(1.0), glm::radians(-90.0f), glm::vec3(0.0, 1.0, 0.0));
这里获得的矩阵M和上面的矩阵view是相同的,能够自行验证下。
也就是说,经过旋转相机+y轴90度,和旋转立方体+y轴-90度,最终计算获得的矩阵相同。调整相机来获得观察效果,能够经过相应的方式来调整物体达到相同的效果。在OpenGL中并不存在真正的相机,这只是一个虚构的概念。
相机坐标系由相机位置eye和UVN基向量(或者说由forward, side ,up)构成,以下图所示:
各个参数的含义以下:
上面的图简化为:
在使用过程当中,咱们是要指定的参数即为相机位置(eye),相机指向的目标位置(target)和viewUp vector三个参数。
Step1 : 首选计算相机镜头方向
进行标准化
Step2: 根据view-up vector和forward肯定相机的side向量:
Step3 : 根据forward和side计算up向量:
这样eye位置,以及forward、side、up三个基向量构成一个新的坐标系,注意这个坐标系是一个左手坐标系,所以在实际使用中,须要对forward进行一个翻转,利用-forward、side、up和eye来构成一个右手坐标系。
咱们的目标是计算世界坐标系中的物体在相机坐标系下的坐标,也就是从相机的角度来解释物体的坐标。从一个坐标系的坐标变换到另外一个坐标系,这就是不一样坐标系间坐标转换的过程。
从坐标和变换一节,了解到,要实现不一样坐标系之间的坐标转换,须要求取一个变换矩阵。而这个矩阵就是一个坐标系A中的原点和基在另外一个坐标系B下的表示。
咱们将相机坐标系的原点和基,使用世界坐标系表示为(s表明side基向量,u表明up基向量,f表明forward基向量):
设方阵A、D可逆,那么分块矩阵
(A0BD) 可逆,且其逆矩阵为T−1=(A−10−A−1BD−1D−1)
这种方式对应的计算代码以下:
// 手动构造LookAt矩阵 方式1
glm::mat4 computeLookAtMatrix1(glm::vec3 eye, glm::vec3 target, glm::vec3 viewUp)
{
glm::vec3 f = glm::normalize(target - eye); // forward vector
glm::vec3 s = glm::normalize(glm::cross(f, viewUp)); // side vector
glm::vec3 u = glm::normalize(glm::cross(s, f)); // up vector
glm::mat4 lookAtMat(
glm::vec4(s.x, u.x, -f.x, 0.0), // 第一列
glm::vec4(s.y, u.y, -f.y, 0.0), // 第二列
glm::vec4(s.z, u.z, -f.z, 0.0), // 第三列
glm::vec4(-glm::dot(s, eye),
-glm::dot(u, eye), glm::dot(f, eye), 1.0) // 第四列
);
return lookAtMat;
}
这种方式求取过程当中涉及到了分块矩阵的逆矩阵计算,若是不习惯,能够看下面的方式2,这是比较经常使用的方式。
求取坐标转换矩阵的过程,也能够从另一个角度出发,即将世界坐标系旋转和平移至于相机坐标系重合,这样这个旋转
其中R就是上面求得的side、up、forward基向量构成的矩阵,以下:
一样计算获得视变换矩阵为:
这种方式对应的计算代码以下:
// 手动构造LookAt矩阵 方式2
glm::mat4 computeLookAtMatrix2(glm::vec3 eye, glm::vec3 target, glm::vec3 viewUp)
{
glm::vec3 f = glm::normalize(target - eye); // forward vector
glm::vec3 s = glm::normalize(glm::cross(f, viewUp)); // side vector
glm::vec3 u = glm::normalize(glm::cross(s, f)); // up vector
glm::mat4 rotate(
glm::vec4(s.x, u.x, -f.x, 0.0), // 第一列
glm::vec4(s.y, u.y, -f.y, 0.0), // 第二列
glm::vec4(s.z, u.z, -f.z, 0.0), // 第三列
glm::vec4(0.0, 0.0, 0.0, 1.0) // 第四列
);
glm::mat4 translate;
translate = glm::translate(translate, -eye);
return rotate * translate;
}
在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));
下面利用这个函数进行一些实验,以帮助理解。在设置相机参数以前,咱们学习下绘制立方体,为实验增长素材。
前面索引绘制矩形一节使用了索引了矩形,若是利用索引绘制立方体,表面上看确实能够节省顶点数据,可是存在的问题是,不能为不一样面上的共同顶点指定不一样的纹理坐标,这在某些状况下会出现问题的。例以下面使用索引绘制的立方体:
因为在正面和侧面的顶点制定了相同的纹理坐标,插值后纹理一致,并无出现可爱的猫咪图案。为此,咱们须要为共用顶点指定不一样的顶点属性,那么解决办法之一是,继续使用顶点数组绘制方式,定义立方体的数据以下:
// 指定顶点属性数据 顶点位置 颜色 纹理
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
};
着色器使用上一节绘制矩形的着色器程序。绘制立方体后,经过设定相机位置随着时间发生改变来观察这个立方体。指定相机位置为在xoz平面圆周运动的点轨迹,代码为:
GLfloat radius = 3.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));
同时在代码中指定投影方式为透视投影,代码为:
// 投影矩阵
glm::mat4 projection = glm::perspective(glm::radians(45.0f),
(GLfloat)(WINDOW_WIDTH) / WINDOW_HEIGHT, 1.0f, 100.0f);
投影方式和投影矩阵的计算将在后面小结介绍,这里只须要知道使用方法便可。
实现绘制立方体,效果以下图所示:
从上面的图中咱们看到了奇怪的现象,立方体后面的部分绘制在了前面的部分上,这种现象是因为深度测试(Depth Test)未开启影响的。深度测试根据物体在场景中到观察者的距离,根据设定的glDepthFunc函数断定是否经过深度测试,默认为GL_LESS,即深度小者经过测试绘制在最终的屏幕上。关于深度测试这个主题,后面会继续学习,这里再也不展开。
OpenGL中开启深度测试方法:
glEnable(GL_DEPTH_TEST);
同时在主循环中,清除深度缓冲区和颜色缓冲区:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
开启深度测试后,旋转相机来观察立方体,效果以下:
上面提到了在指定相机时须要指定相机的viewUP向量,这个向量指定了相机中哪一个方向是向上的。对于相机而言,指定了相机位置eye和相机指向位置target后肯定了相机的指向,位置不变,指向不变时,仍是能够经过改变这个viewUp而影响成像的。这个相似于你眼睛的位置不变,看着的方向不变,可是你能够扭动脖子来肯定哪一个方向是向上,这个viewUp比如头顶给定的方向。相机位置固定在(0,0,3.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);
}
对相机位置随着时间进行改变,能够采用圆的参数方程或者球面参数方程设定。这里只是做为一个示例来设定,能够根据你的具体需求设定对应角度值。示例代码以下:
// 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);
}
例如利用球面坐标方程设定的相机位置,效果以下图所示:
通过视变换后,世界坐标系中坐标转换到了相机坐标系下。须要注意的相机在OpenGL中是个假想的概念,本质是经过矩阵来完成计算的。本节设定相机位置为圆周或者球面运动轨迹,并不能让用户来交互地观察场景中物体,下一节将设计一个第一人称FPS相机,让用户经过键盘和鼠标控制相机,更好地观察场景。