钾肥喵的窝

我在 CODING 部署的 Hexo 博客

0%

[GAMES101 作业2] 三角形与深度缓冲详解

任务清单

  1. 复用作业1中的投影矩阵
  2. 实现三角形栅格化算法
  3. 正确测试点是否在三角形内
  4. (提高作业)使用 MSAA 抗锯齿

任务详解

任务1

求包围盒, 枚举包围盒内像素点, 判断是否在三角形内, 再根据插值的结果判断是否需要更新深度缓冲及帧缓冲.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();

// TODO : Find out the bounding box of current triangle.
// iterate through the pixel and find if the current pixel is inside the triangle

auto get_z_interpolated = [t, v](float x, float y) -> float {
auto [alpha, beta, gamma] = computeBarycentric2D(x, y, 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;
return z_interpolated;
};

// 求包围盒, +-1避免浮点误差, 再取min或max避免越界
int min_x = std::min({ v[0].x(), v[1].x(), v[2].x() }) - 1; min_x = std::max(0, min_x);
int max_x = std::max({ v[0].x(), v[1].x(), v[2].x() }) + 1; max_x = std::min(width - 1, max_x);
int min_y = std::min({ v[0].y(), v[1].y(), v[2].y() }) - 1; min_y = std::max(0, min_y);
int max_y = std::max({ v[0].y(), v[1].y(), v[2].y() }) + 1; max_y = std::min(height - 1, max_y);

Vector3f color = t.getColor();

for (int x = min_x; x <= max_x; x++) {
for (int y = min_y; y <= max_y; y++) {
if (insideTriangle(x + .5f, y + .5f, t.v)) {
float depth = get_z_interpolated(x + .5f, y + .5f);
float &origin_depth = depth_buf[get_index(x, y)];
if (depth <= origin_depth)
{
Vector3f point(x, y, depth);
//更新深度
origin_depth = depth;
//更新所在点的颜色
set_pixel(point, color);
}
}
}
}

// TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
}

调试了半天也不对, 然后发现是作业1中的变换矩阵求错了, 附录对此进行了修正.

任务2

判断叉积是否同号就行了

代码如下(这里对代码框架进行了修改, 参数改为float类型方便后续MSAA的引入):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static bool insideTriangle(float x, float y, const Vector3f* _v)
{
// TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
Vector3f ver(x, y, 0);

int cnt = 0;

for (int i = 0; i < 3; i++) {
Vector3f ver1 = _v[(i + 1) % 3] - _v[i]; ver1.z() = 0;
Vector3f ver2 = ver - _v[i]; ver2.z() = 0;
if (ver1.cross(ver2).z() < 0) {
cnt++;
}
}
return (cnt == 0) || (cnt == 3);
}

任务3

单独维护子像素的缓冲队列, 这样能避免黑边问题.

添加以下成员:

1
2
3
const int sampling_rate = 2;
std::vector<Eigen::Vector3f> frame_sample;
std::vector<float> depth_sample;

更新以下函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void rst::rasterizer::clear(rst::Buffers buff)
{
if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
{
std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
std::fill(frame_sample.begin(), frame_sample.end(), Eigen::Vector3f{ 0, 0, 0 });
}
if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
{
std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
std::fill(depth_sample.begin(), depth_sample.end(), std::numeric_limits<float>::infinity());
}
}

rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
frame_buf.resize(w * h);
depth_buf.resize(w * h);
frame_sample.resize(w * h * sampling_rate * sampling_rate);
depth_sample.resize(w * h * sampling_rate * sampling_rate);
}

然后就可以开始写MSAA版的函数了, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// MSAA
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();

auto get_z_interpolated = [t, v](float x, float y) -> float {
auto [alpha, beta, gamma] = computeBarycentric2D(x, y, 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;
return z_interpolated;
};

int min_x = std::min({ v[0].x(), v[1].x(), v[2].x() }) - 1; min_x = std::max(0, min_x);
int max_x = std::max({ v[0].x(), v[1].x(), v[2].x() }) + 1; max_x = std::min(width - 1, max_x);
int min_y = std::min({ v[0].y(), v[1].y(), v[2].y() }) - 1; min_y = std::max(0, min_y);
int max_y = std::max({ v[0].y(), v[1].y(), v[2].y() }) + 1; max_y = std::min(height - 1, max_y);

Vector3f color = t.getColor();

float step = 1.f / sampling_rate;

for (int x = min_x; x <= max_x; x++) {
for (int y = min_y; y <= max_y; y++) {
int idx = get_index(x, y);
int eid = idx * sampling_rate * sampling_rate;
float _depth = 0;
Vector3f _color(0, 0, 0);
for (int a = 0; a < sampling_rate; a++) {
for (int b = 0; b < sampling_rate; b++) {
int bias = a * sampling_rate + b;
float sub_x = x + (a + .5f) * step;
float sub_y = y + (b + .5f) * step;
if (insideTriangle(sub_x, sub_y, t.v)) {
float depth = get_z_interpolated(sub_x, sub_y);
if (depth <= depth_sample[eid + bias]) {
depth_sample[eid + bias] = depth;
frame_sample[eid + bias] = color;
}
}
_depth += depth_sample[eid + bias];
_color += frame_sample[eid + bias];
}
}
_depth /= sampling_rate * sampling_rate;
_color /= sampling_rate * sampling_rate;
Vector3f point(x, y, _depth);
depth_buf[idx] = _depth;
set_pixel(point, _color);
}
}
}

附录

透视投影变换矩阵的简单推导

透视投影变换可以分成两步, 先把远平面压缩, 再做正交投影变换.

正交投影变换矩阵

因为透视投影变换通常认为远近平面的中心点是 \((0, 0, z)\), 所以可以简化一下正交投影的变换矩阵(这里假定 \(n, f\) 的意义是到原点的距离且看向 \(-z\) 方向, 下同):

\[ \begin{bmatrix} \frac{2}{w} & 0 & 0 & 0 \\ 0 & \frac{2}{h} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & \frac{n+f}{n-f} \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

\(x,y\) 坐标的变换

根据三角形相似很容易得到: \(\frac{y'}{-n}=\frac{y}{z}\), 其中 \(y'\) 是近平面上的点的坐标, 从而 \(y' = -\frac{n}{z}y\)

\(x\) 坐标也同理

据此以及可以构造出初步的矩阵(考虑齐次坐标, 把分母上的 \(z\) 消去)了:

\[ \begin{bmatrix} -n & 0 & 0 & 0 \\ 0 & -n & 0 & 0 \\ 0 & 0 & ? & ? \\ 0 & 0 & 1 & 0 \\ \end{bmatrix} * \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix} = \begin{bmatrix} -nx \\ -ny \\ ? \\ z \\ \end{bmatrix} \]

\(z\) 坐标的变换

考虑 \((x, y, -n, 1)^{T}\)\((x, y, -f, 1)^{T}\) 变换后 \(z\) 坐标不变:

\[ \begin{bmatrix} -n & 0 & 0 & 0 \\ 0 & -n & 0 & 0 \\ 0 & 0 & A & B \\ 0 & 0 & 1 & 0 \\ \end{bmatrix} * \begin{bmatrix} x \\ y \\ -n \\ 1 \\ \end{bmatrix} = \begin{bmatrix} -nx \\ -ny \\ n^{2} \\ -n \\ \end{bmatrix}, \qquad \begin{bmatrix} -n & 0 & 0 & 0 \\ 0 & -n & 0 & 0 \\ 0 & 0 & A & B \\ 0 & 0 & 1 & 0 \\ \end{bmatrix} * \begin{bmatrix} x \\ y \\ -f \\ 1 \\ \end{bmatrix} = \begin{bmatrix} -fx \\ -fy \\ f^{2} \\ -f \\ \end{bmatrix} \]

解下列方程组:

\[ \begin{cases} -nA+B=n^{2} \\ -fA+B=f^{2} \end{cases} \quad \Rightarrow \quad \begin{cases} A=-(n+f) \\ B=-nf \end{cases} \]

透视投影变换矩阵

把两个矩阵乘起来(注意先后顺序, 先压缩, 再正交投影)就得到了透视投影变换矩阵:

\[ \begin{bmatrix} \frac{2}{w} & 0 & 0 & 0 \\ 0 & \frac{2}{h} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & \frac{n+f}{n-f} \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} -n & 0 & 0 & 0 \\ 0 & -n & 0 & 0 \\ 0 & 0 & -(n+f) & -nf \\ 0 & 0 & 1 & 0 \\ \end{bmatrix} = \begin{bmatrix} -\frac{2n}{w} & 0 & 0 & 0 \\ 0 & -\frac{2n}{h} & 0 & 0 \\ 0 & 0 & -\frac{n+f}{n-f} & -\frac{2nf}{n-f} \\ 0 & 0 & 1 & 0 \\ \end{bmatrix} \]