OpenGL其中的一个主要工作是将3D坐标转化为屏幕上的2D像素坐标(离散的)。期间进行复杂的矩阵变换,OpenGL将这个复杂的过程分成几个子过程,每个过程完成特定的计算工作。


对应代码如下:
// ************************************************// ************ 顶 点 着 色 器// ************************************************#version 330 corelayout(location = 0) in vec3 pos; // 顶点局部坐标(在模型中的坐标)uniform mat4 modelMatrix; // 模型矩阵uniform mat4 viewMatrix; // 视图矩阵uniform mat4 projectionMatrix; // 投影矩阵void main(){vec3 localPos = pos; // 局部空间(模型空间)坐标float w = 1.0; // w分量(x、y、z、w)vec4 posVec4 = vec4(localPos, w); // 转换成4D齐次坐标(用4D向量表示3D坐标)vec4 posInWorld = modelMatrix * posVec4; // 模型矩阵变换,得到世界坐标vec4 posInCamera = viewMatrix * posInWorld; // 视图矩阵变换,得到视图坐标vec4 clippingPos = projectionMatrix * posInCamera; // 投影矩阵变换,得到裁剪坐标gl_Position = clippingPos; // 顶点着色器输出裁剪坐标,用于后续高效裁剪。}
一、局部空间
#version 330 corelayout(location = 0) in vec3 pos; // 顶点局部坐标(在模型中的坐标)......void main(){vec3 localPos = pos; // 局部空间坐标......}
就是指物体本地坐标系,也可以叫模型空间。
这个坐标系由模型设计者定义,比如我们使用建模软件(比如Blender)创建一个立方体模型。最简单的情况,我们在建模的时候可以定义坐标系的原点在立方体中心位置,坐标轴分别垂直立方体的两个面,则:
// 立方体的8个顶点的坐标分别是:// 0.5, -0.5, 0.5 右下角-前// 0.5, 0.5, 0.5 右上角-前// -0.5, 0.5, 0.5 左上角-前// -0.5, -0.5, 0.5 左下角-前// 0.5, -0.5, -0.5 右下角-后// 0.5, 0.5, -0.5 右上角-后// -0.5, 0.5, -0.5 左上角-后// -0.5, -0.5, -0.5 左下角-后
二、4D齐次坐标与齐次矩阵
#version 330 corelayout(location = 0) in vec3 pos; // 顶点局部坐标(在模型中的坐标)......void main(){float w = 1.0; // w分量(x、y、z、w)vec4 posVec4 = vec4(localPos, w); // 转换成4D齐次坐标(用4D向量表示3D坐标)......}
将3D坐标转换成4D齐次坐标表示,目的是为了能使用4x4齐次矩阵进行平移变换(沿x、y、z轴方向移动)。
w = 1.0时,表示平移变换生效。
w = 0.0时,将没有平移变换效果,4x4齐次矩阵的变换效果就等同于它左上角3x3矩阵部分的线性变换。
这有什么意义?比如单位法向量只关注方向,无需考虑位置,所以单位法向量的变换无需考虑平移,可以令w=0.0。
三、模型矩阵变换(Model Matrix)
顶点在世界坐标系中的变换(旋转、缩放、平移等),顶点初始位置在世界坐标系原点。
// modelMatrix: 模型矩阵// posVec4: 4D齐次坐标// posInWorld: 世界坐标(其实就是所有模型的顶点都在同一个坐标系中)vec4 posInWorld = modelMatrix * posVec4; // 模型矩阵变换
将所有模型各自模型空间下的局部坐标统一变换到ModelMatrix定义的坐标系中(世界坐标系)。
模型的初始位置:模型的局部坐标系的原点在世界坐标系的原点位置。
右乘ModelMatrix矩阵,相当于模型在世界坐标系中进行各种变换:
- 平移变换:移动模型到指定位置
- 缩放变换:放大或者缩小模型
- 旋转变换:绕任意轴旋转任意角度
- 镜像变换:把模型变成镜子里的镜像(左手和右手调换了)
- 。。变换
总结这一步的工作就是,将模型全部放好位置。
下面代码是如何构建模型矩阵:
glm::mat4 modelMatrix( 1.0f ); // 注意,必须初始化为单位矩阵,而不是零矩阵// 平移效果modelMatrix = glm::translate( modelMatrix, glm::vec3(5.0f, // 在x方向的位移6.0f, // 在y方向的位移7.0f ) ); // 在z方向的位移// 旋转效果modelMatrix = glm::rotate( modelMatrix,glm::radians( 90.0f ), // 旋转90°glm::vec3(0.0f, 1.0f, 0.0f ) ); // 绕的旋转轴// 缩放效果modelMatrix = glm::scale( modelMatrix,glm::vec3( 0.5f, 0.6f, 0.7f ) ); // 分辨在xyz方向上的缩放因子// 最终得到的矩阵的变换效果就是:先平移,再旋转,最后缩放变换。注意顺序不能变动。
四、视图矩阵变换(View Matrix)
// viewMatrix: 视图矩阵// posInWorld: 世界坐标// posInCamera: 摄像机坐标系中的坐标vec4 posInCamera = viewMatrix * posInWorld;// 将世界坐标全部变换到viewMatrix定义的坐标系中(摄像机坐标系)。// 通俗理解就是直播摄影师肩扛着摄像机,找了一个位置拍这些放好的模型。而我们通过屏幕实时观看了拍摄画面。
将世界坐标转换到ViewMatrix定义的坐标系中(摄像机坐标系)。
摄像机坐标系定义如下:
- 原点:摄像机位置。
- 轴正方向:摄像机的“正右方”方向。
- Y轴正方向:摄像机的“正上方”方向。
- Z轴正方向:摄像机的“正后方”方向。
总结这一步的工作,就是确定观察角度。
顺便说一句,在第一人称视角游戏中,我们的视角是不断变化的,因此这个视图矩阵也是不断在改变的。
下面代码是如何构建视图矩阵:
// 待完成
五、投影矩阵变换(Projection Matrix)
// projectionMatrix: 投影矩阵// posInCamera: 摄像机坐标系中的顶点坐标// clipPos: 裁剪坐标,叫裁剪坐标的原因是,这个坐标非常容易判断是否需要被裁剪。vec4 clipPos = projectionMatrix * posInCamera;// 裁剪坐标,特征如下:// 令,clipPos = (x, y, z, w),若满足以下三个条件,// 则clipPos在projectionMatrix定义的视景体(View Volume)内部:// 1、-w <= x <= w// 2、-w <= y <= w// 3、-w <= z <= w// 还要一个重要特性,随着posInCamera.z值的线性增长,clipPos.z的值并不是线性增长,而是增长的越来越慢,比如:// posInCamera.z从0 -> 10 时,clipPos.z从0.0w -> 0.5w。// posInCamera.z从10 -> 100时,clipPos.z从0.5w -> 1.0w。// 这就是深度值的非线性特性,目的是让近景的物体的深度有更高精度。
下面代码是如何构建投影矩阵:
glm::mat4 projection(1.0f);// *********************************// ******* 正 交 投 影 矩 阵// *********************************// 正交投影并没有远近的概念,远的东西和近的东西大小都是一样的// 换句话说,所有的顶点的Z值都相同,正交投影的视景体就是一个矩形projection = glm::ortho(0.0f, 0.0f, // 视景体的左下角位置800.0f, 600.0f); // 视景体的右上角位置// *********************************// ******* 透 视 投 影 矩 阵// *********************************// 透视投影的视景体是一个平截头体(Frustum),削了顶的金字塔形状。// 有6个面(plane):远近、上下、左右。projection = glm::perspective(glm::radians( pCamera->fov() ), // 就是FOV,field of view,视角广度( float ) ScreenWidth / ScreenHeight, // aspectRatio,宽高比0.1f, // near plane,近平面位置,离摄像头更近的那一面100.0f ); // far plane,远平面位置
六、透视除法(Perspective Divde)
图形硬件会自动完成。
将裁剪坐标转化成标准设备空间坐标(Normalized Device Coordinate, NDC),即x、y、z分量都在[ -1.0, 1.0 ]之间。做法很简单。
当为正交投影时,。
当为透视投影时,顶点坐标离观察点越远,分量越大,相同的距离映射到屏幕上的距离就会越小,这就是实现透视效果(近大远小)的原理。
七、视口变换(Viewport Transform)
图形硬件会自动完成。
接下来我们需要将NDC坐标映射到一个指定大小的矩形显示区域(视口)中。比如视口大小刚好是窗口大小,则相当于把NDC坐标转换成屏幕坐标。
GLsizei ScreenWidth = 800; // 屏幕宽高GLsizei ScreenHeight = 600;glViewport( 0, 0, // 视口左下角在窗口中的坐标ScreenWidth, ScreenHeight ); // 视口宽高// 将NDC坐标映射到800x600的屏幕上的屏幕坐标。
八、矩阵相关
矩阵根据内部元素的排序规则分两种类型:
- 列优先(Column Major)
- 行优先(Row Major)
OpenGL采用列优先的矩阵,主要原因是OpenGL想用一维数组来构造矩阵,而不是二维数组(速度更慢),C中的二维数组是行优先规则。
因此向量也都是列向量形式。根据线性代数规则,应该是,而不能
。
模型视图变换矩阵
根据线性代数矩阵特性,我们可以讲模型矩阵和视图矩阵相乘得到模型视图变换矩阵,它其实就是先执行模型变换再执行视图变换的效果。
点击查看【processon】
