1、坐标系统
-
右手坐标系:伸开右手,大拇指指向X轴正方向,食指指向Y轴正方向,其他三个手指指向Z轴正方向
-
左手坐标系:伸开左手,大拇指指向X轴正方向,食指指向Y轴正方向,其他三个手指指向Z轴正方向
两者的区别主要是两者Z轴的方向是相反的,openGL中使用的是右手坐标系
每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。
这里需要理解坐标体系和空间的区别
openGL中主要的几种空间:
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
我的理解是在不同的空间下,对应不同的坐标系
1.1 坐标体系
openGL中主要的几种坐标系
-
世界坐标系
以屏幕中心为原点(0,0,0),当你面对屏幕时,右边是X正轴,上方是Y轴正轴,屏幕指向你的方向为Z轴正轴。窗口范围是从(-1,1),即屏幕左下角坐标为(-1,-1,0),右上角坐标为(1,1,0)。我们用这个坐标系描述物体及光源的位置
将物体放到场景中(平移、旋转等),这些操作就是坐标变换。openGL中提供了glTranslate / glScale / glRotate 三条坐标变换命令,利用变换矩阵运算命令,则可以实现任意复杂的坐标变换 -
惯性坐标系
由世界坐标系和物体坐标系联合理解,是物体坐标系的旋转,只是一个中间状态的描述,方便物体坐标系切换到世界坐标系 -
局部空间坐标系
是以物体某一点为原点建立的坐标,该坐标仅对该物体适用,用来简化对物体各部分坐标的描述。物体放到场景中时,各部分经历的坐标变换相同,相对位置不变,可以视为一个整体 -
摄像机坐标系:观察者坐标系
以观察者为原点,视线的方向为Z轴的正方向。openGL管道会将世界坐标先变换到观察者坐标,然后进行裁剪。只有视线范围内的场景才会进行下一步的计算
1.2 空间变换(坐标体系转换)
为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是如下三个矩阵:
- 模型(Model)
- 观察(View)
- 投影(Projection)
我们的顶点坐标起始于局部空间(Local Space)
- 称为局部坐标(Local Coordinate)
它在之后会变为
- 1.世界坐标(World Coordinate)
- 2.观察坐标(View Coordinate)
- 3.裁剪坐标(Clip Coordinate)
并最后以屏幕坐标(Screen Coordinate)的形式结束。如下图所示:
个人理解:物体处于局部控功能件中,经过模型变换后(旋转平移缩放这些),我们采用世界空间表示模型, 再经过透视视图变换,投影变换后,进入裁剪体系,最后转换成屏幕坐标,输出给屏幕。
参考:openGL中的坐标系
1.3 投影
投影分类:
- 正射投影
- 透视投影
区别如下图:
正射投影矩阵直接将坐标映射到2D平面中,即你的屏幕,但实际上一个直接的投影矩阵会产生不真实的结果,因为这个投影没有将透视(Perspective)考虑进去。所以我们需要透视投影矩阵来解决这个问题。
在GLM中可以这样创建一个透视投影矩阵:
glm::perspective(radians, scale, nearDis, farDis)
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
- 第一个参数radians指的是Fov,它表示的是视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f。他就是图中两个蓝色实线的空间夹角。
- 第二个参数scale设置了宽高比,由视口的宽除以高所得。
- 第三和第四个参数设置了平截头体的近和远平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。图中粉色截面即为近平面,蓝色截面即为远平面。
正投影矩阵的创建方法:glm::ortho(oriX1, oriX2, oriY1, oriY2, nearDis, farDis); 一般很少用到
1.4 组合
每一个步骤都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被变换到裁剪坐标:
注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)
最后的顶点应该被赋值到顶点着色器中的gl_Position
,OpenGL将会自动进行透视除法和裁剪。
那么如何在OpenGL代码中表示这样一个关系呢?
顶点着色器,所有的坐标都是输出在顶点着色器的gl_Position
:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
定义观察矩阵:
glm::mat4 view;
view = glm::translate(view, glm::vec3(0.0f, 0.0f, 0.0f));
如果不经常改变透视视图的话,只需要在绘图循环之外定义透视投影就行:
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);
更新shader中对应矩阵值:
shader.setMtx4fv("view", view);
shader.setMtx4fv("projection", projection);
整体用伪代码来表示:
glm::mat4 view = glm::mat4(1.0f); //观察矩阵
glm::mat4 projection = glm::mat4(1.0f); //投影矩阵
//进行旋转平移缩放等操作
view = glm::translate(view, glm::vec3(0.f, 0.f, -3.f));
//设置投影矩阵
projection = glm::perspective(glm::radians(45.0f), (float)screenWidth/(float)screenHeight, 0.1f, 100.0f);
//更新shader
shader.setMtx4fv("view", view);
shader.setMtx4fv("projection", projection);
2、摄像机
当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。
摄像机就好像是我们的眼睛,我们从摄像机的方向观察世界空间中的模型。摄像机远离模型,模型自然就变小了(透视投影下),然而,在GL中事实上并没有摄像机的概念。但是
我们可以通过移动世界空间远离我们的摄像机来模拟摄像机远离世界的感觉
。
4张图理解摄像机:
摄像机位置(图一)
这个很好理解,比如你规定了地面上一个物体作为世界的原点,那么随着你眼睛相对远点的位置改变时,你所看到的物体的样子也会随之改变。所以眼睛相对于远点的位置会直接影响物体的样子,同理,我们也需要确定摄像机相对于世界空间坐标原点的位置
。这里你可以看下上面图中的第一个图片来理解。坐标系就是世界空间,我们要确定的就是摄像机在世界空间中的位置。
摄像机的方向(图二)
你站在一个位置不动,你将目光集中在物体上不同的点时,你所看到的物体也不同。或者说,你目光的方向改变时,物体也跟着改变。同理,我们还需要确定摄像机观察的方向
。第二张图中就显示了摄像机在当前位置看向世界空间远点的示例。
摄像机的滚转角(图三)
好了,现在你站在一个位置不动,目光也一直盯着物体的中心点不动,你还可以让你看到的物体改变。除了闭眼睛,你还可以歪一下头,你看到的东西是不是斜过来了(你非说没变那是因为强大的大脑已经帮你转换回来了又,你可以把眼睛换成手机摄像头然后倾斜手机再看看,手动滑稽)。所以,我们要确定摄像机在世界空间中摆放的夹角
。这么表述可能不清楚,稍微借一点坐标系的概念。我们摄像机的方向就是观察坐标系的Z轴。但是一个Z轴确定却并不能确定一个坐标系,我们至少要确定两个坐标轴,才能通过两个坐标轴确定第三个坐标轴从而建立一个坐标系。第四张图就显示了确定三个轴夹角后的坐标系。
观察坐标系
经过上面的论述,我们知道了,我们只要知道摄像机的位置
、摄像机的方向
和x或y轴中任意一个轴的方向
即可确定。
在接下来的教程里,我们会假定摄像机的滚转角不再改变。这样做的好处是无论摄像机的方向是怎样的,摄像机坐标系的x轴与世界坐标系的y轴总是空间垂直的,同理,摄像机坐标系的y轴与世界坐标系的x轴也总是空间垂直的。有了这个特点,我们可以很方便的确定一个摄像机坐标系。
如果我们不改变滚转角保证了摄像机坐标系的x轴与世界坐标系的y轴总是空间垂直,我们就可以通过摄像机方向向量与世界坐标系的y轴的方向向量叉乘从而获得摄像机坐标系的x轴
(两向量叉乘将获得同时垂直于两个向量的第三个向量)。
LookAt
我们已经知道如何去构建一个摄像机坐标系了,不过怎么通过这些元素构建出观察矩阵呢?glm为我们提供了LookAt(position,target,up)函数。它含有三个参数:
- position,第一个参数就是我们摄像机在世界坐标系的位置了
- target,第二个参数是我们观察的点的位置,就是我们目光汇聚的那个点了,为什么是目标点呢?因为通过position减去target我们就可以获得摄像机方向的向量了
- up,第三个参数是一个与摄像机坐标系x轴垂直的向量。为什么是这个向量呢?因为我们可以通过position和target确定摄像机的方向,也就是摄像机坐标系z轴。再找到一个也与x轴垂直的向量即可确定x轴的方向向量了。
摄像机的up向量(上轴,正y轴)就是与摄像机坐标系x轴垂直的向量,只需要知道x轴向量和z轴向量,通过叉乘就可以获得y向量
GLM中生成LookAt矩阵:
glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
上面函数中,描述了我们的摄像机在世界坐标系的(0,0,3)位置,我们观察的点就是世界坐标原点,这个up向量就是世界坐标系的y轴的方向向量。
事实上,如果我们的滚转角不发生改变的话,那么我们所需要的up向量就一直是(0,1,0)。
360旋转
我们将target一直设置为世界坐标原点,改变摄像机自身位置,就可以做到旋转的效果。
比如:
float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
上述代码就是根据时间变化,改变(x,z)的值,也就是改变摄像机坐标,target坐标一直是原点,这样就可以达到随时间360°旋转。
自由移动、水平运动
通过键盘或其他输入设备,随时修改摄像机位置,达到自由移动的效果
视角移动
我们在上述有个前提,up向量是固定的,这就造成不能转头也不能抬头。为了保证我们的焦距是不变的,所以我们要将up向量标准化。
现在我们的摄像机坐标系与世界坐标系的各轴是平行的。我们想抬头呢,我们就以x轴旋转坐标系,想左右牛头就以y轴旋转坐标系即可。
欧拉角
欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:
pitch
:俯仰角
是描述我们如何往上或往下看的角,可以在第一张图中看到。yaw
: 第二张图展示了偏航角
,偏航角表示我们往左和往右看的程度。roll
:滚转角
代表我们如何翻滚摄像机,通常在太空飞船的摄像机中使用。
每个欧拉角都有一个值来表示,把三个角结合起来我们就能够计算3D空间中任何的旋转向量了。
对于我们的摄像机系统来说,我们只关心俯仰角和偏航角,所以我们不会讨论滚转角。给定一个俯仰角和偏航角,我们可以把它们转换为一个代表新的方向向量的3D向量。
LearnOpenGl教程的解释似乎很难理解,评论区老哥画的图:
首先我们了解一下欧拉角是如何计量的:
-
俯仰角θ(pitch)
:机体坐标系X轴与水平面的夹角。当X轴的正半轴位于过坐标原点的水平面之上(抬头)时,俯仰角为正,否则为负。pitch是围绕X轴旋转,也叫做俯仰角。 -
偏航角ψ(yaw)
:机体坐标系xb轴在水平面上投影与地面坐标系xg轴(在水平面上,指向目标为正)之间的夹角,由xg轴逆时针转至机体xb的投影线时,偏航角为正,即机头右偏航为正,反之为负。yaw是围绕Y轴旋转,也叫偏航角 -
翻滚角Φ(roll)
:机体坐标系zb轴与通过机体xb轴的铅垂面间的夹角,机体向右滚为正,反之为负。roll是围绕Z轴旋转,也叫翻滚角
现在我们知道如何计量欧拉角了,现在我们把它放在世界坐标系中的原点。因为我们想以Y轴负方向为默认视角,所以我们的摄像机是朝向z轴负轴摆放。现在我们开始计算欧拉角。
direction.x = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = - cos(glm::radians(pitch)) * cos(glm::radians(yaw));
那么我们如何来改变这个角度呢?我们可以接收鼠标输入。或是其他人机交互设备