我是靠谱客的博主 快乐乌冬面,最近开发中收集的这篇文章主要介绍【GAMES101】作业2--三角形光栅化前言作业要求解答代码总结结果,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 前言
  • 作业要求
    • 额外说明
    • 评分
  • 解答
    • 1 复制粘贴上节课的get_projection_matrix
    • 2 判断像素点是否在三角形内部insideTriangle
      • 对应知识
      • 对应代码
    • 3 光栅化
      • 步骤概览
      • 1. 找到Bounding Box
      • 2. 在Bounding box 内遍历所有元素,判断是否在三角形内部
      • 3. 根据已有代码来得到深度值,也就是z_interpolated
      • 4. 如果当前位置深度比depth_buf更小,则更新颜色值并保存深度值
  • 代码总结
  • 结果

前言

本文为GAMES101现代计算机图形学入门 的学习笔记系列。

我们的系列笔记将分为两部分:

  1. 课堂笔记
  2. 作业

原课程为2020年2月闫令琪所教授的 GAMES101 现代计算机图形学入门

课程主页:https://sites.cs.ucsb.edu/~lingqi/teaching/games101.html
(幻灯片和课程录像均在此处)

课程共计22节。作业共计8次。

针对人群:计算机图形学入门新手

教材
Steve Marschner and Peter Shirley的"Fundamentals of Computer Graphics"
第三版或更新版本。目前无官方中文版。
民间翻译:https://www.stubbornhuang.com/1812/

笔记目录


2022-6-9

作业要求

在上次作业中,虽然我们在屏幕上画出一个线框三角形,但这看起来并不是那么的有趣。所以这一次我们继续推进一步——在屏幕上画出一个实心三角形,换言之,栅格化一个三角形。上一次作业中,在视口变化之后,我们调用了函数rasterize_wireframe(const Triangle& t)。但这一次,你需要自己填写并调用函数 rasterize_triangle(const Triangle& t)。

该函数的内部工作流程如下:

  1. 创建三角形的 2 维 bounding box。
  2. 遍历此 bounding box 内的所有像素(使用其整数索引)。然后,使用像素中心的屏幕空间坐标来检查中心点是否在三角形内。
  3. 如果在内部,则将其位置处的插值深度值 (interpolated depth value) 与深度缓冲区 (depth buffer) 中的相应值进行比较。
  4. 如果当前点更靠近相机,请设置像素颜色并更新深度缓冲区 (depth buffer)。

你需要修改的函数如下:

  • rasterize_triangle(): 执行三角形栅格化算法
  • static bool insideTriangle(): 测试点是否在三角形内。你可以修改此函数的定义,这意味着,你可以按照自己的方式更新返回类型或函数参数。

额外说明

因为我们只知道三角形三个顶点处的深度值,所以对于三角形内部的像素,我们需要用插值的方法得到其深度值。我们已经为你处理好了这一部分,因为有关这方面的内容尚未在课程中涉及。插值的深度值被储存在量 z_interpolated中。

请注意我们是如何初始化 depth buffer 和注意 z values 的符号。为了方便同学们写代码,我们将 z 进行了反转,保证都是正数,并且越大表示离视点越远。

在此次作业中,你无需处理旋转变换,只需为模型变换返回一个单位矩阵。最后,我们提供了两个 hard-coded 三角形来测试你的实现,如果程序实现正确,你将看到如下所示的输出图像:
在这里插入图片描述

在你自己的计算机或虚拟机上下载并使用我们更新的框架代码。你会注意到,在 main.cpp 下的 get_projection_matrix() 函数是空的。请复制粘贴你在第一次作业中的实现来填充该函数。

评分

  • [5 分] 正确地提交所有必须的文件,且代码能够编译运行。
  • [20 分] 正确实现三角形栅格化算法。
  • [10 分] 正确测试点是否在三角形内。
  • [10 分] 正确实现 z-buffer 算法, 将三角形按顺序画在屏幕上。
  • [提高项 5 分] 用 super-sampling 处理 Anti-aliasing : 你可能会注意到,当我们放大图像时,图像边缘会有锯齿感。我们可以用 super-sampling来解决这个问题,即对每个像素进行 2 * 2 采样,并比较前后的结果 (这里并不需要考虑像素与像素间的样本复用)。需要注意的点有,对于像素内的每一个样本都需要维护它自己的深度值,即每一个像素都需要维护一个 sample list。最后,如果你实现正确的话,你得到的三角形不应该有不正常的黑边。

解答

我们需要填写的两个函数都在rasterize.cpp当中。并且我们需要复制粘贴上一次作业写的get_projection_matrix。

我们首先需要写一个判断像素点是否在三角形内部的算法。这个函数的返回值就是一个bool变量。这个函数名为static bool insideTriangle()

rasterize_triangle()函数是用来三角形栅格化的。它分为以下三步:

  1. 创建三角形的boundingBox。
  2. 在boundingBox内判断像素点坐标是否在三角形内(利用上面写的insideTriangle函数)
  3. 通过深度插值,从三角形三个顶点的深度值找到每个像素的深度值。(这一点代码已经被给出)
  4. 如果当前位置深度比depth_buf(类rasterizer的一个成员)更小,则更新颜色值。

1 复制粘贴上节课的get_projection_matrix

我们先复制粘贴get_projection_matrix
如下

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();

    // TODO: Implement this function
    // Create the projection matrix for the given parameters.
    // Then return it.

    // 1. 透视投影转为正交投影矩阵
    float n = zNear;
    float f = zFar;
    Eigen::Matrix4f M_persp2Ortho;
    M_persp2Ortho << n, 0, 0, 0,
		0, n, 0, 0,
		0, 0, n + f, -n * f,
		0, 0, 1, 0;

    // 2. 正交投影转换到正则立方体
    float fov = eye_fov * MY_PI / 180.0;

    float t = -n * tan(fov / 2.);
    float b = -t;
    float r = aspect_ratio * t;
    float l = -r;

    Eigen::Matrix4f M_ortho, trans, scale;
    trans << 1, 0,  0,  -(r+l)/2,
             0, 1,  0,  -(t+b)/2,
             0, 0,  1,  -(n+f)/2,
             0, 0,  0,  1;
    scale << 2/(r-l),   0,  0,  0,
                0,  2/(t-b),   0,  0,
                0,  0,  2/(n-f),    0,
                0,  0,  0,  1;
    M_ortho =  scale * trans;
            
    projection = M_ortho * M_persp2Ortho ;
    return projection;
}

2 判断像素点是否在三角形内部insideTriangle

题目中给出的API如下

static bool insideTriangle(int x, int 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]
}

我们接受一个x,接受一个y,这代表了当前点的像素编号(整数)。接受一个向量引用 _v,这代表了三角形的三个顶点坐标。

对应知识

课堂笔记2–向量与线性代数
在这里插入图片描述
在这里插入图片描述

对应代码

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]
    const Eigen::Vector2f P(x, y);
    const Eigen::Vector2f A = _v[0].head(2), B = _v[1].head(2), C = _v[2].head(2);

    const Eigen::Vector2f AP = P - A;
    const Eigen::Vector2f BP = P - B;
    const Eigen::Vector2f CP = P - C;
    const Eigen::Vector2f AB = B - A;
    const Eigen::Vector2f BC = C - B;
    const Eigen::Vector2f CA = A - C;

    float eq1 = AB[0] * AP[1] - AB[1] * AP[0];
    float eq2 = BC[0] * BP[1] - BC[1] * BP[0]; 
    float eq3 = CA[0] * CP[1] - CA[1] * CP[0];

    if(  eq1 > 0 && eq2 > 0 && eq3>0)
        return true;
    else if(eq1 < 0 && eq2<0 && eq3<0)
        return true;
    else 
        return false;
}

注意以下几点:

  1. 应该注意const Vector3f* _v这个参数
    这个参数是一个Vector3f指针,实际上代表的是一个数组。数组中的每个元素是一个Vector3f类型,
    也就是代表了一个顶点的坐标。每个点用_v[0],_v[1],_v[2]表示。而每个_v[0]就是一个Vector3f类型的点。整个数组代表了多个点。这里一般认为传入的是三个点,即一个三角形。

  2. 注意我们做的是2D模拟,因此只是传入了x、y坐标。而Vector3f是一个3D的坐标,因此我们只使用它的前两个坐标。在Eigen当中就是使用.head(2)函数

  3. 二维的向量对于Eigen来说是无法做叉乘的。Eigen只支持三维叉乘。所以我们要手动写叉乘。
    a × b = ( a 1 , a 2 ) T × ( b 1 , b 2 ) T = a 1 b 2 − a 2 b 1 atimes b = (a1, a2)^T times (b1, b2)^T \ = a1b2 - a2 b1 a×b=(a1,a2)T×(b1,b2)T=a1b2a2b1
    请注意,上面的写法是不严谨的,因为叉乘是不改变张量的阶的。向量叉乘,得到的应该还是向量。所以这里只是表示了大小,而没有表示方向。实际上,方向是指向垂直于纸面(向外或者内)。如果想要严谨地写出来,只需要把第三个分量写为0,然后按照3D向量叉乘公式(也就是行列式),如下,写出来即可。你会发现只剩下了k基矢的项。
    在这里插入图片描述

3 光栅化

步骤概览

上文说到

rasterize_triangle()函数是用来三角形栅格化的。它分为以下4步:

  1. 创建三角形的boundingBox。
  2. 在boundingBox内判断像素点坐标是否在三角形内(利用上面写的insideTriangle函数)
  3. 通过深度插值,从三角形三个顶点的深度值找到每个像素的深度值。(这一点代码已经被给出)
  4. 如果当前位置深度比depth_buf(类rasterizer的一个成员)更小,则更新颜色值。

我们依次来做这四步(其中第三步已经被写好了)

1. 找到Bounding Box

怎么找呢?其实就是找三角形的x坐标和y坐标的最大最小值而已!
比如下面这张图
我们的bounding box就是所在范围的9个格子。
在这里插入图片描述
对应的代码就是

    //1. 找到Bounding Box
    float xmin = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
    float ymin = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
    float xmax = std::max(std::max(v[0].x(), v[1].x()), v[2].x());
    float ymax = std::max(std::max(v[0].y(), v[1].y()), v[2].y());

注意:根据课堂笔记6–光栅化(深度测试与抗锯齿),其实坐标值和整数编号值只是差了0.5

我们找bounding box,是为了接下来做for循环遍历。所以假如找到的是float类型的,不那么方便循环。因为坐标值和整数编号值只是差了0.5,所以我们就往外拓展一点点boundingbox。如下面的代码所示。

在这里插入图片描述

    //我们将bounding box稍微扩大一点,得到整数值,方便循环
    xmin = (int)std::floor(xmin);
    xmax = (int)std::ceil(xmax);
    ymin = (int)std::floor(ymin);
    ymax = (int)std::ceil(ymax);

2. 在Bounding box 内遍历所有元素,判断是否在三角形内部

这一步很简单,就是写两个for循环,然后再利用第一步写了的insideTriangle函数来判断

    //2. 在Bounding box 内遍历所有元素,判断是否在三角形内部
    for (int x = xmin; x <= xmax; x++)
    {
        for (int y = ymin ; y <= ymax; y++)
        {
            //像素的坐标值只是比整数编号值大0.5而已。
            if (insideTriangle(x + 0.5, y + 0.5, t.v))
            {

我们这里唯一要注意的,就是insideTriangle函数的使用方法

这个函数接收三个参数,前两个就是坐标值的x,y而已。刚才已经说了,坐标值就是像素编号+0.5而已。

第三个参数实际上是一个数组,这个数组的每个元素都是一个3维Vector3f类型的点。

我们现在所拥有的参数,是const Triangle& t,这是一个三角形,是代码框架自定义的类型。它的类型定义位于Triangle.hpp

class Triangle{

public:
    Vector3f v[3]; /*the original coordinates of the triangle, v0, v1, v2 in counter clockwise order*/
	...
};

我们只要看它的第一个成员变量,叫做v,它恰好是一个数组,数组的每个元素是个Vector3f的点。这就对应上我们需要的参数。

3. 根据已有代码来得到深度值,也就是z_interpolated

直接取消注释已有的代码即可

                //3.根据已有代码来得到深度值,也就是z_interpolated    
                // If so, use the following code to get the interpolated z value.
				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;

这一步的目的,就是把三角形三个顶点的深度值插值成三角形内每个像素点的深度值

4. 如果当前位置深度比depth_buf更小,则更新颜色值并保存深度值

很简单,假如该点当前的深度值比buffer中的更小,就更新颜色,并且保存新的深度值。

这就是z-buffer算法。这个算法很简单,其实就是寻找最小值而已。找到了最小值,就覆盖原来的最小值,并且更新颜色。depth_buf保存的就是原来的最小值。

                //4. 如果当前位置深度比depth_buf(类rasterizer的一个成员)更小,则更新颜色值。
                if (z_interpolated < depth_buf[get_index(x, y)])
                {
                    //TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
                    set_pixel(Vector3f(x, y, z_interpolated), t.getColor());
                    depth_buf[get_index(x, y)] = z_interpolated;
                }

我们这里要注意2点:

  1. depth_buf的使用方法。
    depth_buf是自定义的Triangle类型的一个成员变量。
std::vector<float> depth_buf;

它就是个一维浮点数数组而已。使用方法就是用数组下标。但是这里要注意了:我们的像素编号是两个(x和y),所以要先把它转换成一个。转换的方法通俗易懂,就是一行行地排排坐,排完一行再排一行。从左到右,从上到下。

如图所示
在这里插入图片描述

甚至代码框架里面已经给你写好了转换的函数

int rst::rasterizer::get_index(int x, int y)
{
    return (height-1-y)*width + x;
}

这里的height和width也是整数。实际上,由于OpenCV也许是从左上到右下排列的,所以它可能用的是(height-1-y)。这里就是把数字转换一下。

但是这些都不用我们操心,我们只要使用get_index函数就行了。

  1. set_pixel的使用方法
void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)

它接收两个参数,第一个就是点坐标,第二个就是RGB颜色。

第一个参数我们就给当前像素点的坐标即可,Vector3f(x, y, z_interpolated)
第二个参数,我们就给t.getColor()

class Triangle{

public:
    Vector3f v[3]; /*the original coordinates of the triangle, v0, v1, v2 in counter clockwise order*/
    /*Per vertex values*/
    Vector3f color[3]; //color at each vertex;
    ...
    Vector3f getColor() const { return color[0]*255; } // Only one color per triangle.
    ...
};

这个函数就是Triangle所需要绘制的颜色值。

代码总结

第一个函数

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]
    const Eigen::Vector2f P(x, y);
    const Eigen::Vector2f A = _v[0].head(2), B = _v[1].head(2), C = _v[2].head(2);

    const Eigen::Vector2f AP = P - A;
    const Eigen::Vector2f BP = P - B;
    const Eigen::Vector2f CP = P - C;
    const Eigen::Vector2f AB = B - A;
    const Eigen::Vector2f BC = C - B;
    const Eigen::Vector2f CA = A - C;

    float eq1 = AB[0] * AP[1] - AB[1] * AP[0];
    float eq2 = BC[0] * BP[1] - BC[1] * BP[0]; 
    float eq3 = CA[0] * CP[1] - CA[1] * CP[0];

    if(  eq1 > 0 && eq2 > 0 && eq3>0)
        return true;
    else if(eq1 < 0 && eq2<0 && eq3<0)
        return true;
    else 
        return false;
}

第二个函数

//Screen space rasterization
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

    //1. 找到Bounding Box
    float xmin = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
    float ymin = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
    float xmax = std::max(std::max(v[0].x(), v[1].x()), v[2].x());
    float ymax = std::max(std::max(v[0].y(), v[1].y()), v[2].y());

    //我们将bounding box稍微扩大一点,得到整数值,方便循环
    xmin = (int)std::floor(xmin);
    xmax = (int)std::ceil(xmax);
    ymin = (int)std::floor(ymin);
    ymax = (int)std::ceil(ymax);

    //2. 在Bounding box 内遍历所有元素,判断是否在三角形内部
    for (int x = xmin; x <= xmax; x++)
    {
        for (int y = ymin ; y <= ymax; y++)
        {
            //像素的坐标值只是比整数编号值大0.5而已。
            if (insideTriangle(x + 0.5, y + 0.5, t.v))
            {
                //3.根据已有代码来得到深度值,也就是z_interpolated    
                // If so, use the following code to get the interpolated z value.
				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;

                //4. 如果当前位置深度比depth_buf(类rasterizer的一个成员)更小,则更新颜色值。
                if (z_interpolated < depth_buf[get_index(x, y)])
                {
                    //TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
                    set_pixel(Vector3f(x, y, z_interpolated), t.getColor());
                    depth_buf[get_index(x, y)] = z_interpolated;
                }

            }
        }
    }   
}

结果

在这里插入图片描述

最后

以上就是快乐乌冬面为你收集整理的【GAMES101】作业2--三角形光栅化前言作业要求解答代码总结结果的全部内容,希望文章能够帮你解决【GAMES101】作业2--三角形光栅化前言作业要求解答代码总结结果所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(41)

评论列表共有 0 条评论

立即
投稿
返回
顶部