Assignment 1: 3D Transformation
概述
电子计算机通过二维的显示屏向用户展示其计算的内容和数据,透过这个二维的屏幕,我们能看到的并不仅仅是二维的内容,还有三维的广阔空间。这其中蕴含了怎样的三维空间变换到二维空间的奥妙?本次作业将带领你们通过手动的编程实践来切身体会三维空间的物体是如何显示到二维的屏幕上的。通过本次实践,你将领略到线性代数的魅力所在!
在目前的基于光栅化的渲染管线中,从三维的二维的转换,历经的变换矩阵包含模型矩阵、观察矩阵、投影矩阵和视口变换矩阵。学习了可编程管线的OpenGL之后,你应该知道这些变换通常是在顶点着色器发生的(视口变换在顶点着色器之后),矩阵变换作用的对象是三维物体的几何顶点数据。本次的作业要求你手动实现观察矩阵、投影矩阵和视口变换矩阵的计算,并学会用旋转、缩放等变换来对实现对三维物体的空间操纵。渲染器的绝大部分代码都已经实现好,你只需要在我们指定的地方填补代码空白即可,实际的代码量不大。
Task 1、实现观察矩阵(View Matrix)的计算
该函数在 main.cpp 中被调用:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.5f, 3.7f);
glm::vec3 lookAtTarget = glm::vec3(0.0f);
renderer->setViewMatrix(TRRenderer::calcViewMatrix(cameraPos, lookAtTarget,
glm::vec3(0.0, 1.0, 0.0f)));
根据课件的推导,观察矩阵M可写成两个矩阵R,T相乘
变换实质是移动物体,而非移动相机,要等价于把相机移到原点,再看向-Z轴,再旋转使up转向y轴
R是旋转矩阵,T是位移矩阵,点都采用齐次坐标表示。
glm::mat4 TRRenderer::calcViewMatrix(glm::vec3 e, glm::vec3 g, glm::vec3 t)
{
//Setup view matrix (world space -> camera space)
glm::mat4 vMat = glm::mat4(1.0f);
glm::mat4 R = glm::mat4(1.0f);
glm::mat4 T = glm::mat4(1.0f);
glm::vec3 f(normalize(g - e));
glm::vec3 s(normalize(cross(t, f)));
glm::vec3 u(cross(s, f));
R[0][0] = s.x;
R[1][0] = s.y;
R[2][0] = s.z;
R[0][1] = u.x;
R[1][1] = u.y;
R[2][1] = u.z;
R[0][2] = -f.x;
R[1][2] = -f.y;
R[2][2] = -f.z;
R[3][3] = 1.0f;
T[3][0] = -e.x;
T[3][1] = -e.y;
T[3][2] = -e.z;
vMat = R * T;
//Task 1: Implement the calculation of view matrix, and then set it to vMat
// Note: You can use any glm function (such as glm::normalize, glm::cross, glm::dot) except glm::lookAt
return vMat;
}
Task 2、实现透视投影矩阵(Project Matrix)的计算
该函数在 main.cpp 中被调用:
renderer->setProjectMatrix(TRRenderer::calcPerspProjectMatrix(45.0f,
(float)width/ height, 0.0, 50.0f));
投影矩阵推导的结果如下,传入FOV和纵横比,near和far距离,推算出对应平头截体每个点的齐次坐标
glm::mat4 TRRenderer::calcPerspProjectMatrix(float fovy, float aspect, float near, float far)
{
//Setup perspective matrix (camera space -> clip space)
glm::mat4 pMat = glm::mat4(1.0f);
//Task 2: Implement the calculation of perspective matrix, and then set it to pMat
// Note: You can use any math function (such as std::tan) except glm::perspective
pMat[0][0] = 1.0f / aspect / tan(fovy / 2);
pMat[1][1] = 1.0f / tan(fovy / 2);
pMat[2][2] = -(far + near) / (far - near);
pMat[3][3] = 0.0f;
pMat[2][3] = -1.0f;
pMat[3][2] = -2.0f * far * near / (far - near);
return pMat;
}
Task3、实现视口变换矩阵(Viewport Matrix)的计算,该函数在 TRRenderer.cpp 文件中。
视口变换是将(-1,1)规范化坐标范围映射到nx*ny个像素组成的屏幕坐标
通过缩放再平移的方式实现,推导结果如下
glm::mat4 TRRenderer::calcViewPortMatrix(int width, int height)
{
//Setup viewport matrix (ndc space -> screen space)
glm::mat4 vpMat = glm::mat4(1.0f);
vpMat[0][0] = width / 2;
vpMat[1][1] = height / 2;
vpMat[3][0] = (width - 1) / 2;
vpMat[3][1] = (height - 1) / 2;
//Task 3: Implement the calculation of viewport matrix, and then set it to vpMat
return vpMat;
}
完成前3个task后结果如下(见1.gif),生成一只会旋转的怪兽
Task4、程序默认物体绕着 y y y轴不停地旋转,请你在 main.cpp 文件中稍微修改一下代码,使得物体分别绕 x x x轴和 z z z轴旋转。贴出你的结果。
static glm::mat4 model_mat = glm::mat4(1.0f);
//Rotation
{
//Task 4: Make the model rotate around x axis and z axis, respectively
model_mat = glm::rotate(model_mat, (float)deltaTime * 0.001f, glm::vec3(0,
1, 0));
}
查看glm::rotate函数,功能是绕向量v旋转角度angle。
将v分别修改为x轴,z轴即可
绕x轴旋转(见2.gif)model_mat = glm::rotate(model_mat, (float)deltaTime * 0.001f, glm::vec3(1, 0, 0));
绕z轴旋转(见3.gif)model_mat = glm::rotate(model_mat, (float)deltaTime * 0.001f, glm::vec3(0, 0, 1.0f));
Task5、仔细体会使物体随着时间的推进不断绕 y y y轴旋转的代码,现在要你用 glm::scale 函数实现物体不停地放大、缩小、放大的循环动画,物体先放大至 2.0 2.0 2.0倍、然后缩小至原来的大小,然后再放大至 2.0 2.0 2.0,依次循环下去,要求缩放速度适中,不要太快也不要太慢。贴出你的效果,说说你是怎么实现的。(请注释掉前面的旋转代码)
定义变大矩阵big,每过deltaTime的时间,可以使怪兽每次变大1.02倍
再定义缩小矩阵small, 每次可以使怪兽变小到0.97倍
维护大小bas,及怪兽正在变大还是变小
//Scale
{
model_mat = glm::rotate(model_mat, (float)deltaTime * 0.001f, glm::vec3(0, 1, 0));
glm::mat4 big = glm::mat4(1.0f) , small= glm::mat4(1.0f);
big[0][0] = 1.02f;
big[1][1] = 1.02f;
big[2][2] = 1.02f;
small[0][0] = 0.97f;
small[1][1] = 0.97f;
small[2][2] = 0.97f;
//Task 5: Implement the scale up and down animation using glm::scale function
if (bas > 2.0)
fat = 1;
if (bas < 1.0)
fat = 0;
if (fat == 0)
{
model_mat = model_mat * big;
bas *= 1.02f;
}
else
{
model_mat = model_mat * small;
bas *= 0.97f;
}
}
结果如下,见4.gif
Task6、现在要求你实现正交投影矩阵的计算,该函数在 TRRenderer.cpp 文件中。
根据课件上的推导,正交投影矩阵如下
矩阵第2行第3列应为(f+n)/(n-f)
先将xyz坐标大小缩放到[-1,1]的区间,再将中点平移到原点
glm::mat4 TRRenderer::calcOrthoProjectMatrix(float l ,float r, float b, float t, float n, float f)
{
//Setup orthogonal matrix (camera space -> homogeneous space)
glm::mat4 pMat = glm::mat4(1.0f);
pMat[0][0] = 2.0f / (r - l);
pMat[1][1] = 2.0f / (t - b);
pMat[2][2] = 2.0f / (n - f);
pMat[3][0] = (l + r) / (l - r);
pMat[3][1] = (b + t) / (b - t);
pMat[3][2] = -(f + n) / (f - n);
//Task 6: Implement the calculation of orthogonal projection, and then set it to pMat
return pMat;
}
Task7、实现了正交投影计算后,在 main.cpp 的如下代码中,分别尝试调用透视投影和正交投影函数,通过滚动鼠标的滚轮来拉近或拉远摄像机的位置,仔细体会这两种投影的差别。
结果如下(见5.gif)
滑动滚轮图像没有改变。
Task8、完成上述的编程实践之后,请你思考并回答以下问题:
(1)请简述正交投影和透视投影的区别。
透视投影看到的是平头截体内的点,可由fov和纵横比确定。
在透视投影中,有近大远小,通过滚轮调整摄像机的远近,怪兽会变大变小。
正交投影看到的是设定长方体内的点,相当于把长方体压扁,忽略z坐标,直接投影到xOy平面上
在正交投影中,无论远近,物体的大小是固定的,比如底座的正方形,在正交投影中,就必然是平行四边形
(2)从物体局部空间的顶点的顶点到最终的屏幕空间上的顶点,需要经历哪几个空间的坐标系?裁剪空间下的顶点的 w w w值是哪个空间坐标下的 z z z值?它有什么空间意义?
单个物体的点有局部坐标
与其他与物体的相对位置,构成世界坐标
通过视图矩阵得到观察空间坐标,每个点坐标都从摄像机角度观察
后投影到剪切坐标,裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
最后通过视口变换得到屏幕坐标
正交投影后,裁剪空间的w值仍为1
透视投影中,剪切空间的w值是观察空间的 -z。满足 -w≤x≤w 、-w≤y≤w、-w≤z≤w的点才落在目标的平头截体内。
(3)经过投影变换之后,几何顶点的坐标值是被直接变换到了NDC坐标(即 x y z xyz xyz的值全部都在 [ − 1 , 1 ] [-1,1] [−1,1]范围内)吗?透视除(Perspective Division)是什么?为什么要有这么一个过程?
个人理解,投影变换后得到剪切空间。剪切坐标经过透视除法得到NDC坐标。
透视除法将齐次坐标化为标准化坐标,并只保留-1<=x<=1、-1<=y<=1的点。
透视除法将点的坐标除以深度,全部投影到z=1的平面上。