预备知识:线性代数、高等数学
目录
一、变换
1.矩阵变换
2D变换
缩放
拉伸
旋转
变换都可写成矩阵形式
对于2D有
2D_point=(x,y,1)^T
2D_vector=(x,y,0)^T
对于3D有
3D_point=(x,y,z,1)^T
3D_vector=(x,y,z,0)^T
3D齐次坐标表示
2.视图变换
将虚拟世界中以(x,y,z)为坐标的物体变换到 以一个个像素位置(x,y) 来表示的屏幕坐标系之中(2维)
模型变换:旋转,平移,缩放
摄像机变换:
相机或眼睛位置 (eye postion) e
观察方向 (gaze postion) g
视点正上方向 (view-up vector ) t
投影变换
正交投影:坐标的相对位置都不会改变,全部转换到一个的空间之中
透视投影:透视投影就是最类似人眼所看东西的方式,遵循近大远小
先压缩后正交投影,变换矩阵为:
最后将将这个被压缩过的空间,重新正交投影成标准小立方体,故定义透视投影变换:
补充:视图变换
模型变换(modeling tranformation):将一个物体自身进行变换(缩放、旋转、位移)
视角变换(view tranformation):根据眼睛来判断物体的相对位置
投影变换(projection tranformation):将三维空间内的物体投影至标准二维平面([-1,1]^2)之上
视口变换(viewport transformation):将处于标准平面映射到屏幕分辨率范围之内,即[-1,1]^2→\rightarrow→[0,width]*[0,height], 其中width和height指屏幕分辨率大小
旋转变换:
视口变换:
games101 作业1
Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
// TODO: Implement this function
// Create the model matrix for rotating the triangle around the Z axis.
// Then return it.
Eigen::Matrix4f tran;
float angle = rotation_angle / 180.0 * MY_PI;
tran << cos(angle), -1 * sin(angle), 0, 0,
sin(angle), cos(angle), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1;
model = tran * model;
return model;
}
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar)
{
// Students will implement this function
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
Eigen::Matrix4f per2orth;
per2orth << zNear, 0, 0, 0,
0, zNear, 0, 0,
0, 0, zNear + zFar, -zNear * zFar,
0, 0, 1, 0;
float angle = eye_fov * MY_PI / 180.0;
float t = zNear * tan(angle / 2.0);
float b = -t;
float r = t * aspect_ratio;
float l = -r;
Eigen::Matrix4f orth;
orth << 2.0 / (r - l), 0, 0, 0,
0, 2.0 / (t - b), 0, 0,
0, 0, 2 / (zNear - zFar), 0,
0, 0, 0, 1;
Eigen::Matrix4f trans;
trans << 1, 0, 0, -(r + l) / 2.0,
0, 1, 0, -(t + b) / 2.0,
0, 0, 1, -(zNear + zFar) / 2.0,
0, 0, 0, 1;
projection = projection * orth * trans * per2orth;
// TODO: Implement this function
// Create the projection matrix for the given parameters.
// Then return it.
return projection;
}
二、光栅化
光栅化的目的就是将想要展现的物体给真正现实到屏幕上的过程
1 屏幕像素的表示
屏幕中的每一个像素点我们都用整数坐标进行表示,最大最小值与分辨率相对应,考虑到每个像素都有一定的面积,我们定义(x+0.5,y+0.5)为该(x,y)像素的中心,如图中黑圈所示。
像素:
1)是一个一个的小的方块.
2)每个方块内的颜色是完全一致的,像素就是最小的单位了,像素内部不会再发生变化。
3)我们用256个等级0-255来表示灰度(如等级0表示是黑的,等级255表示是白的),一个像素内的颜色可以用
rgb(red,green,blue)三个值来定义,用红绿蓝的各种组合来表示每一种红色蓝色绿色的密度,例如红色是(255,0,0),白色是(255,255,255)。4)一个像素内颜色不会发生任何差异。
屏幕空间就是以屏幕左下角为原点建立一个二维坐标系
1)用(x,y)表示像素的位置。(蓝色像素表示为(2,1)坐标来表示)
2)如果一个屏幕分辨率为width * height,那么我们定义像素下标从(0,0)到(width - 1,height - 1),
且(x,y)像素的中心在(x + 0.5, y + 0.5),如蓝色像素的中心为(2.5,1.5)。
3)屏幕范围从(0,0)到(width,height)
现在我们要将一个中心在坐标原点,棱长为2的立方体显示在长为height、宽为width的屏幕中
具体操作要进行一次视口变换,准确地说是通过缩放与平移变换,
2.光栅化算法
光栅化就是考虑如何将图像呈现在光栅显示器中.般将图像拆分成许多不同的三角形🔺进行显示。
之所以选择三角形是因为
1)三角形是最最基础的多边形。 1)三角形的三个点连接在一起,其三点一定在一个平面上。 2)三角形的内外部定义的很清楚,可以通过向量的叉积判断点是否在三角形内部。 3)三角形顶点插值的一种定义良好的方法。(用来之后求重心坐标的方法) 2)任何多边形均可以拆解成多个三角形。
DDA数值微分算法
任意两点时都可以用y = k x + b 来表示,其中k代表斜率,如果∣ k ∣ < 1 那么它的主要行进方向就是x轴,即x轴的变化要比y轴快,相反如果如果∣ k ∣ > 1那么它的主要行进方向就是y轴,即y轴的变化要比x轴快。
中点Bresenham算法
定义f ( x , y ) = y − k x − b
此时我们取这两个橙色方格的中点,如图中圆圈符号所对应的那个点,倘若这个点在直线方程的下面,那么很明显我们应该选择右上的方格。
三角形光栅化算法
对屏幕中的每一个像素进行采样,如果这个像素点在三角形之中那么这个像素点就应该被采用
判断点在三角形内
利用叉乘判别:如图所示,我们事先知道想要光栅化的三角形的三个顶点P0,P1,P2,以及检测点Q。只要分别计算如果三者同号则代表点P在三条线段的同一边,那么必然处于三角形内部,如果不同号则代表该点一定在三角形外部。
static bool insideTriangle(int x, int y, const Vector3f* _v)
{
//.head(2)指这个点的前两个数值,即x,y
Eigen::Vector2f q;
q << x, y;
Eigen::Vector2f a, b, c,v0_q,v1_q,v2_q;
a = _v[1].head(2) - _v[0].head(2);
b = _v[2].head(2) - _v[1].head(2);
c = _v[0].head(2) - _v[2].head(2);
v0_q = q- _v[0].head(2);
v1_q = q- _v[1].head(2);
v2_q = q - _v[2].head(2);
return a[0] * v0_q[1] - a[1] * v0_q[0] > 0 && b[0] * v1_q[1] - b[1] * v1_q[0] > 0 && c[0] * v2_q[1] - c[1] * v2_q[0]>0;
// TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
}
3.抗锯齿
我们在采样的时候的频率过低无法跟上图像的频率,导致最后结果的失真。
从简单的角度去解释这种问题出现的原因就是,我们用有限离散的像素点去逼近连续的三角形,那么自然会出现这种锯齿走样的现象,因为这种近似是不准确的。
超采样反走样 SSAA
把原来的每个像素点进行细分,采样点越多抗锯齿效果越好,但计算负担也会随之增加
多采样反走样 MSAA
MSAA其实是对SSAA的一个改进
我们依然同样会分采样点,但是只会去计算究竟有几个采样点会被三角形cover,计算颜色的时候只会利用像素中心坐标计算一次颜色(即所有的信息都会被插值到像素中心然后取计算颜色)
4.Z-Buffer算法
判断物体先后关系:选离摄像头最近的像素点显示
1. Z-Buffer算法需要为每个像素点维持一个深度数组记为zbuffer,其每个位置初始值置为无穷大(即离摄像机无穷远)。
2. 随后我们遍历每个三角形面上的每一个像素点[x,y],如果该像素点的深度值z,小于zbuffer[x,y]中的值,则更新zbuffer[x,y]值为该点深度值z,并同时更新该像素点[x,y]的颜色为该三角形面上的该点的颜色。
三、着色
物体能被我们观察,是因为人眼接收到了从物体来的光
1. 镜面反射
2. 漫反射
3. 环境光
1.漫反射
漫反射便是光从一定角度入射之后从入射点向四面八方反射,且每个不同方向反射的光的强度相等
只有当入射光线与平面垂直的时候才能完整的接受所有光的能量,而入射角度越倾斜损失的能量越大,具体来说,我们应该将光强乘上一个cos=l*n,其中l是入射光方向,n为平面法线方向。
除了入射角度之外,光源与照射点的距离也应该考虑,直观来说,离得越远当然强度也就越弱
图中中心为一个点光源,光线均匀的向周围发射,可以想象光源发射出来的能量其实是一定的,那么在任意两个圈上接受到的能量之和一定相等。而离圆心越远,圆的面积越大,单位面积所接受能量也就越弱
模拟漫反射
其中kd为漫反射系数,i入射光强,n.l分别如图中所示为法线向量和入射方向,max是为了剔除夹角大于90°的光。
2.高光
只有当观察方向集中在反射方向周围很近的时候才能看见反射光,因此在镜面反射中会考虑 R 与 v的夹角α
其中ks为镜面反射系数,I为入射光强,r为光源到入射点距离,注意这里在max剔除大于90°的光之后,我们还乘了一个指数p,添加该项的原因很直接,因为离反射光越远就越不应该看见反射光,需要一个指数p加速衰减
环境光
环境光+漫反射+高光:
3.着色方法
Flat Shading:以每一个面作为一个着色单位
Gouraud Shading:对每个三角形的顶点进行一次着色,将所有共享这个点的面的法线向量加起来求均值,最后再标准化就得到了该顶点的法线向量了,对于三角形内部的每一个点有:
其中c 0 , c 1 , c 2为三角形三个顶点的颜色α,β,γ为三角形面内一点的重心坐标,c 为该点的插值之后得到的颜色。
Phong Shading:对于三角形内部的每一个点着色
补:重心坐标
图形渲染管线
顶点处理:对所有的顶点数据进行Model,View,和Projection的变换,最终得到投影到二维平面的坐标信息
三角形处理:将所有的顶点按照原几何信息,变成三角面,每个面由3个顶点组成
光栅化
片元处理
作业3
phong_fragment_shader:最基础的着色模型为Blinn-Phong反射模型。
高光+漫反射+间接光或环境光
- 我们现在考虑光照是对任何一个点而言,假设这个点叫做shading point,那么这个点的着色结果是什么?我们定义在一个极小的范围内,它是一个平面,即与它所在的曲面相切的平面
- 既然是平面,那么就有法线n垂直于平面
- 同样我们还可以定义一个观测方向,我们规定从shading point到相机的方向为观测方向v
- 同样道理,从shading point到光源的方向称为光照方向l
- 注意,因为我们只关心这些向量的方向,所以它们都是单位向量,长度为1
texture_fragment:比phong_fragment_shader多贴图