基础
Inside Triangle
直接利用计算几何中求凸包时所用到的 In-Trangle Test
以及 To-Left Test
即可,此处已知点的坐标,使用行列式计算三角形的有向面积。
代码
static bool toLeft(std::pair<float, float>& p, std::pair<float, float>& q, std::pair<float, float>& s) {
// Area2 determinant
return
p.first * q.second - p.second * q.first
+ q.first * s.second - q.second * s.first
+ s.first * p.second - s.second * p.first > 0.f;
}
static bool insideTriangle(int x, int y, const Vector3f* _v)
{
std::pair<float, float> p, q, r, s;
p = {_v[0].x(), _v[0].y()}; q = { _v[1].x(), _v[1].y()}, r = { _v[2].x(), _v[2].y() }, s = {x, y};
bool pq_left = toLeft(p, q, s);
bool qr_left = toLeft(q, r, s);
bool rp_left = toLeft(r, p, s);
return (pq_left == qr_left) && (pq_left == rp_left);
}
Rasterize Triangle
整体的算法是:
- 首先计算出三角形的包围盒(
BB
); - 然后枚举所有
BB
内的像素(下标取整):- 判断其中心(下标
+
0.5
+\ 0.5
+ 0.5)是否在三角形内部:
- 如果在,那就通过透视矫正插值计算出
z_interpolated
,再与当前位置的depth_buf
进行比较(注意此处框架的z
全部是正值,而非负值,越加靠近摄像机就越小):- 如果离摄像机更近,则更新当前位置的
frame_buf
和depth_buf
;
- 如果离摄像机更近,则更新当前位置的
- 如果在,那就通过透视矫正插值计算出
- 判断其中心(下标
+
0.5
+\ 0.5
+ 0.5)是否在三角形内部:
代码
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
int x_min = floor(std::min(v[0].x(), std::min(v[1].x(), v[2].x())));
int x_max = ceil(std::max(v[0].x(), std::max(v[1].x(), v[2].x())));
int y_min = floor(std::min(v[0].y(), std::min(v[1].y(), v[2].y())));
int y_max = ceil(std::max(v[0].y(), std::max(v[1].y(), v[2].y())));
for (int i = x_min; i < x_max; ++i) {
for (int j = y_min; j < y_max; ++j) {
if (insideTriangle(i + 0.5f, j + 0.5f, t.v)) {
float alpha, beta, gamma;
auto tu = computeBarycentric2D(i, j, t.v);
std::tie(alpha, beta, gamma) = tu;
// 一个透视矫正的深度插值,看看推导!
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
int ind = get_index(i, j);
if (z_interpolated < depth_buf[ind]) {
Eigen::Vector3f point(i, j, 1.0); // set point
set_pixel(point, t.getColor()); // set color
depth_buf[ind] = z_interpolated; // update depth buffer
}
}
}
}
}
效果
一点笔记
在计算 z_interpolated
时,使用了如下代码:
auto[alpha, beta, gamma] = computeBarycentric2D(i, j, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
此处为啥这么写?
-
因为要求在透视投影之前的各点的深度,来实现
Z-Buffer
; -
而这是一个透视矫正的深度插值:
三角形重心在做透视投影的前后都满足以下的插值公式:
但经过推导(可见 Reference 第一项),使用透视投影后的插值系数也能够计算出透视投影之前的重心插值
且被插值的属性 I I I 可以由下式计算得到:
此处插值的是深度,替换
I
I
I 为深度,而
Z
A
,
Z
B
,
Z
C
Z_A, Z_B, Z_C
ZA,ZB,ZC 等均为透视投影前的深度,根据透视变换矩阵可知:
储存在 Vector4f
的第四维,所以在计算
Z
Z
Z 以及调用
Z
A
,
Z
B
,
Z
C
Z_A, Z_B, Z_C
ZA,ZB,ZC 时使用的都是 w
;
-
可是为什么这么写?既然都知道了 I A , I B , I C I_A, I_B,I_C IA,IB,IC,直接用 α , β , γ \alpha, \beta, \gamma α,β,γ 带入插值公式去算 I I I 不香嘛?
事实上 α , β , γ \alpha, \beta, \gamma α,β,γ 并不好算,由计算公式:
可知,需要透视投影前三角形顶点的另外两维数据,即
X
,
Y
X,Y
X,Y,可事实上由于使用的是 Vector4f
,在经过了 MVP变换 后只留下了透视投影之前的
Z
A
,
Z
B
,
Z
C
Z_A,Z_B,Z_C
ZA,ZB,ZC,无法直接使用该公式计算
α
,
β
,
γ
\alpha, \beta, \gamma
α,β,γ,除非做一次逆变换;
所以只能换个方法,通过透视投影后的三角形的三个顶点的
X
′
,
Y
′
X', Y'
X′,Y′,计算出
α
′
,
β
′
,
γ
′
\alpha', \beta', \gamma'
α′,β′,γ′,再借以 Vector4f
中存储的透视投影前的
Z
A
,
Z
B
,
Z
C
Z_A, Z_B, Z_C
ZA,ZB,ZC,计算出
Z
Z
Z,再得到
α
,
β
,
γ
\alpha, \beta, \gamma
α,β,γ:
提高
可以看到在蓝色三角形的边缘存在一些走样( A l i a s i n g Aliasing Aliasing),可以通过一些反走样( A n t i − A l i a s i n g Anti-Aliasing Anti−Aliasing)的算法来解决;
-
在频域上,做的就是滤波,滤掉高频,使得频率满足奈奎斯特采样定理;
-
由数字信号处理可知,而频域上的乘法就是时域上的卷积,所以表现在图像上,就是对像素做一些平均(卷积操作),使得边缘更加模糊,看起来不那么锐利。
4xSSAA
- 对于三角形的包围盒内部的每一个像素,遍历他的四个
child pixel
;- 如果在三角形内,则更新
child pixel
对应的frame_buf_4xSSAA
和depth_buf_4xSSAA
;
- 如果在三角形内,则更新
- 由于有多个三角形存在,不能立刻更新
frame_buf
,只能在所有的三角形处理完毕后,再进行对每个像素的平均;- 在
draw
函数的最后,遍历所有像素,每个像素的frame_buf
等于四个child pixel
所对应的frame_buf_4xSSAA
的和,再求平均。
- 在
代码
for (int i = x_min; i < x_max; ++i) {
for (int j = y_min; j < y_max; ++j) {
if (_4xSSAA) {
float dx[] = { 0.25f, 0.75f, 0.75f, 0.25f }, dy[] = { 0.25f, 0.25f, 0.75f, 0.75f };
for (int k = 0; k < 4; k++)
{
float x = i + dx[k], y = j + dy[k];
if (insideTriangle(x, y, t.v)) {
auto [alpha, beta, gamma] = computeBarycentric2D(x, y, t.v); // alpha', beta', gamma'
float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w()); // Z
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
int ind = get_index(i, j);
if (z_interpolated < depth_buf_4xSSAA[ind][k]) {
// u cannot update the frame buffer here, because 2 triangles exist
frame_buf_4xSSAA[ind][k] = t.getColor();
depth_buf_4xSSAA[ind][k] = z_interpolated; // update depth buffer of 4xSSAA
}
}
}
}
else { /* trivial */ }
}
}
效果
可以看到蓝色三角形的走样不再那么明显:
4xMSAA
- 对于三角形的包围盒内部的每一个像素,遍历他的四个
child pixel
;- 如果在三角形内,则覆盖率 + 25 % +25\% +25%,该像素的颜色由三角形的颜色以及该像素的覆盖率所决定;
- 事实上由于多个三角形存在,需要考虑混合颜色,或者插值。