games101-三角形光栅化与抗锯齿

三角形光栅化和简单抗锯齿

一个场景中的模型,投影到我们屏幕上的过程就像是拍照。先把物体摆放好(M),然后设置好相机(V),形成透视关系(P),最后按下快门(视口)。这一系列做好之后,再进行光栅化,图像就会呈现到我们的屏幕上

首先进行MVP变换和视口变换。MVP变换是下面三种变换的总称

  • Model:模型变换

    模型变换是将物体从局部空间变换到世界空间的一个矩阵,也就是将模型从局部坐标系转换到世界坐标系。在世界空间中,物体具有*移、缩放和旋转三种操作,对应T(Translate)矩阵、R(Rotate)矩阵、和S(Scale)三个矩阵。将坐标点和变换矩阵相乘就可以得到变换后的结果

    对于一个二维图像中的点,我们假设对它进行线性变换

    x=ax+byy=cx+dy

    写成矩阵形式为

    [xy]=[abcd][xy]

    我们常用的线性变换都可以用这种形式,列如缩放、切变,旋转等

    但是当我们需要*移的时候,就会发现一个问题

    假设我们进行下图中的*移

x=x+txy=y+ty

此时可以发现,我们只能将矩阵写成下面的形式

[xy]=[abcd][xy]+[txtx]

因为无论如何,通过乘的方式得到的项一定是ax+by的形式(*移变换不属于线性变换),而这种形式是不利于计算的。我们不希望*移(仿射变换)成为一种特例,所以我们就需要引入一个新的概念:齐次坐标

对于二维的点和向量,我们增加一个维度,将它表示成如下的形式

2Dpoint=(x,y,1)T2Dvector=(x,y,1)T

则对于之前所述的变换,就有

(xyw)=(10tx01ty001)(xy1)=(x+txy+ty1)

(向量的第三维度的坐标为0是为了保护向量的*移不变性)

补充:

vector + vector = vector

point - point = vector

point + vector = point

point + point = ?

前三个没有什么问题,第四个我们得到的是两个点的中点

(xyw)=(x/wy/w1)

两个点在上述的公式下相加,第三维度的1会变成2,要把二变回1,就要除2。故表示的是中点

好了,回到之前的问题,我们现在开始表示S、R、T矩阵

S和T与二维情况类似

Scale=[sx0000sy0000sz00001]Translate=[100tx010ty001tz0001]

对于R,需要进行单独的讨论

我们先讨论绕x轴旋转的

y=sin(θ+α)=sinθcosα+cosθsinαz=cos(θ+α)=cosθcosαsinθsinα

将y和z分别乘cosθ和sinθ

ycosθ=sinθcosθcosα+cos2θsinαzsinθ=sinθcosθcosαsin2θsinα

两个式子做差,可以得到

sinα=ycosθzsinθ

同理,y和z分别乘sinθ和cosθ,相加可以得到

cosα=ysinθ+zcosθ

根据刚才推导得出的式子,则有

Rx(θ)=[10000cosθsinθ00sinθcosθ00001]

对于其它的坐标轴,使用相似的方法可以得到

Ry(θ)=[cosθ0sinθ00100sinθ0cosθ00001]

Rz(θ)=[cosθsinθ00sinθcosθ0000100001]

(旋转过特定轴使用罗德里格斯旋转公式,先不讨论)

由于坐标系的转换,T矩阵、R矩阵、和S矩阵三个矩阵的顺序会影响我们变换的结果。假设说我们先进行*移,再进行旋转,我们可以发现,由于旋转矩阵的轴心点为(0,0,0),而*移之后模型的原点与世界坐标系的原点不再重合,于是我们会得到错误的结果

所以我们必须按照S、R、T的顺序(其它的组合通过简单证明可以发现也会存在上述的问题)来形成M矩阵

Mmodelworld=TRS

则有以下代码

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.
    float x = rotation_angle * MY_PI / 180;
    Eigen::Matrix4f r;
    r<<cos(x),-sin(x),0,0,
    sin(x),cos(x),0,0,
    0,0,1,0,
    0,0,0,1;
    model = r * model;


    return model;
}
  • View:视图变换

    经过模型变换,物体已经变换到世界坐标系下。这时候我们就要确定相机的位置,并进行视图变换

    其实这里的相机,我个人理解为就是确定一个观测位置,我们后续所做的就是把在这个观测位置看到的东西投影到我们的屏幕上。所以我们每确定一个观测矩阵,就代表着我们确定了一个相机。这里我们只使用一个相机

    为了便于计算,我们需要把相机移动到世界坐标系的原点位置。在空间中,如果相机和模型进行相同的变换,那么他们之间的位置关系不会发生变化。也就是说,让我们的观测位置和模型进行相同的变换,使得在将相机移动到原点的同时,观察到的模型形象不会变化(类似两个相同速度的并列物体相互观测对方不会发生变化)

    此时再去对相机展开一些较为细致的思考。举个例子,我们自己就是一台摄像机,前方有一个物体。我们可以前后左右移动,也可以看向任意的方向,也可以进行左右歪头。所以,对于相机,有一个位置向量e来确定位置。然后我们认为相机位于位置向量的顶端,在这里,我们规定了向量g为相机的朝向,表示了向量看向的方向;向量t为相机的向上方向,表示了歪头的情况(可爱捏);最后,因为是三维空间,我们使用两个向量就可以表示出相机处的位置状态,所以叉乘在得到下图中绿色的向量,便于表示而已,没有意义。

这样,我们就可以细化视图变换。首先,根据向量e把相机移动到原点,然后根据tg以及那个绿色的向量,让他们旋转回到x、y、z轴,并且让物体和相机进行完全相同的操作。这样,相机移动到了原点,以原点作为观测位置看到的形态和没有变换前也是一样的。

则我们可以写出

Mview=RviewTview

e移动到原点并不难实现,只是简单的*移

Tview=[100xe010ye001ze0001]

但旋转就不好处理了,所以我们用一些新的方法

假设一个二维的旋转矩阵

Rθ=(cosθsinθsinθcosθ)

如果我们旋转θ角,旋转矩阵就会变为

Rθ=(cosθsinθsinθcosθ)=RθT

定义有旋转θ角,就是旋转θ角得到的旋转矩阵的逆

Rθ=Rθ1(definition)

而刚才我们发现,旋转矩阵的逆等于旋转矩阵的转置(正交矩阵)。所以,对于我们要将相机旋转回坐标轴的操作,我们可以先计算将x、y、z旋转到相机位置的矩阵,再将这个矩阵转置,就可以得到将相机旋转回坐标轴的旋转矩阵了

Rview1=[xg^×t^xtxg0yg^×t^ytyg0zg^×t^ztzg00001]Rview=[xg^×t^yg^×t^zg^×t^0xtytzt0xgygzg00001]

则可以写出以下代码

Eigen::Matrix4f get_rotation(Vector3f axis,float angle){

    float rotation_angle_radian = angle * MY_PI / 180;
    //identity matrx
    Eigen::Matrix3f I = Eigen::Matrix3f::Identity();
    Eigen::Matrix3f N;
    N<<0,-axis(2),axis(1),
    axis(2),0,-axis(0),
    -axis(1),axis(0),0;
    

}
  • Projection:投影变换

    在视图和投影都完成之后,物体已经摆放到了我们想要的位置,并且已经转换到摄像机视角下了。我们就要进行最后一步变换——投影变换,利用投影矩阵,将摄像机看到的画面投影到指定大小的空间内

    投影分为正交投影和透视投影。正交投影就是直接把物体从原来的视图空间转换到坐标系中心[1,1]3的一个立方体中

我们将原来的视图空间假设为上图中的长方体,用远*、上下、左右六个变量来表示它。首先将其*移到原点,然后直接缩放为原点的立方体即可

Mortho=[2rl00002tb00002nf00001][100r+l2010t+b2001n+f20001]

正交投影是一种*行投影,类似于使用一束*行光把我提的影像垂直地投射到地面上。透视投影则可以产生*大远小的效果

假设可视区域类似于左图,我们在左图中规定*和远两个*面。假设我们把远*面挤压到**面的大小,那么如果说在远**面上各有一个完全一样的物体,那么这个处在远*面的物体就会看起来小一些。所以我们可以尝试将视锥中位于*远*面中的所有*面都按照一定比例缩放,挤压成**面的大小。由于距离**面*的*面受挤压的程度小,距离远的挤压程度大,将缩放形成的右图中的长方体进行正交投影,就可以形成类似*大远小的效果

为了能够完成这个挤压的操作,我们在这里做一些定义

  1. **面永远不变
  2. 远*面的z轴坐标不会发生变化
  3. 远*面的中心点永远不变

在挤压的过程中,我们可以通过相似三角形发现一些规律

上图是我们在侧面对视锥的观察情况。由图中关系可知,视锥中的任意一个*面都与**面存在上述的关系。y坐标是这样,x也同理

我们要求出坐标的投影矩阵,但是我们目前并不清楚z的情况

先将投影矩阵大体情况写出来。x和y的变化情况我们已经知道,所以我们可以写出上图中的变换矩阵

这时,我们使用我们刚才的定义。因为**面上所有的点都不会发生变化,在进行透视投影时可以写出下面的矩阵

(xyn1)(xyn1)==(nxnyn2n)

整个投影矩阵我们只有第三行是未知的,所以我们对第三行进行单独讨论

(00AB)(nxnyn2n)=n2

又因为远*面中心点(0,0,f)在投影变换过程中也不会发生变化在进行透视投影时可以写出下面的矩阵

(00f1)(00f1)==(00f2f)

对矩阵的第三行进行单独讨论,也会得到*似于**面的结果。联立可得

An+B=n2Af+B=f2

则我们可以解得第三行中两个位置变量的值

(n0000n0000n+fnf0010)

则可以写出以下代码

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();
    float x = eye_fov * MY_PI / 180;
    float t = tan(x /2) * abs(zNear);
    float r = aspect_ratio * t;
    float l = -r;
    float b = -t;
    Eigen::Matrix4f translation;
    translation<<1,0,0,-(r+l)/2.0f,
    0,1,0,-(t+b)/2.0f,
    0,0,1,-(zNear+zFar)/2.0f,
    0,0,0,1;

    Eigen::Matrix4f scale;
    scale<<2.0f/(r-l),0,0,0,
    0,2.0f/(t-b),0,0,
    0,0,2.0f/(zNear-zFar),0,
    0,0,0,1;

    projection = scale * translation;

    Eigen::Matrix4f M;
    M<<zNear,0,0,0,
    0,zNear,0,0,
    0,0,zNear+zFar,-zNear*zFar,
    0,0,1,0;

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

    return projection;
}

至此,我们已经完成了MVP变换,物体和相机都已经准备好

下一步,我们就要将在已经处于原点立方体中的所有物体显示到屏幕上,也就是进行光栅化

先对一些要用到的内容进行定义

通常使用垂直可视角度和宽高比表示出我们的视锥大小

也有下面的关系

在图形学中,我们认为屏幕是一个像素的二维数组,而这个二维数组的大小就是分辨率(resolution)。屏幕是一种典型的光栅成像设备

我们在这里将像素作为一个长宽为一的小方块(其实不是)来理解,一个像素有固定的颜色,用r\g\b表示。我们用每一个像素块的中心点来表示这个像素的坐标

而经常提到的光栅化(raster),就是把我们上面已经处理完成的场景呈现在屏幕上,也就是把上文中的立方体呈现在屏幕上

我们先进行视口变换,处理x和y坐标轴,直接将这个立方体的宽高拉伸为height和width即可(由于立方体的中心在原点,所以我们也要进行*移,让图像在经过拉伸和*移之后变成类似于上图中的模样)

(width200width20height20height200100001)

然后,我们要将现在处于屏幕上的类似于照片的情景打散成为像素。这里我们使用不同的三角形去拼成画面,再用屏幕中的像素去显示三角形,就能将图片呈现出来,所以,在进行光栅化的时,显示这些三角形成为了必不可少的一步,我们需要考虑我们要显示的三角形和屏幕像素点之间的位置关系

先介绍通过采样的方法实现。通过像素中心点进行采样,判断像素中心点是否在三角形内。我们先讨论二维的情况,直接遍历屏幕上三角形所在区域,如果中心点在三角形内,这个像素点就染色。

可以通过叉积的方法判断。三角形的三个顶点以顺时针的顺序两两组成一个向量,并用每个向量的起始点与要进行判断的点组成一个新的向量。使三个向量都与新的向量进行相乘,得到的结果如果同号,就证明点在三角形内部

这里也可以用老师的判断方法(注意!要用小数值传递,抗锯齿拆分出来小数坑了我一晚上)

static bool insideTriangle(float x, float y, const Vector3f* _v){
    Vector3f v[3];
    for(int i=0;i<3;i++)
        v[i] = {_v[i].x(),_v[i].y(), 1.0};
    Vector3f f0,f1,f2;
    f0 = v[1].cross(v[0]);
    f1 = v[2].cross(v[1]);
    f2 = v[0].cross(v[2]);
    Vector3f p(x,y,1.);
    if((p.dot(f0)*f0.dot(v[2])>0) && (p.dot(f1)*f1.dot(v[0])>0) && (p.dot(f2)*f2.dot(v[1])>0))
        return true;
    return false;
}

将原来的二维*面升维,并将三角形的三个顶点和需要判断的点转换为起点为原点,指向顶点和待判断点的四个向量。三角形的每两个向量组成一个*面,和原来的三角形*面形成了一个三棱锥。此时可以想象,如果待判断点在三角形内部,则原点指向这个点的向量也在三棱锥内部,并且这个向量和其它三个向量组成的三个*面的法向量的叉积都是同号的,反之则会由一个异号

此时可以得到类似于这样的图,有很严重的锯齿。出现这个问题的原因是因为我们对于信号的采样率不够,导致了走样。我们要进行抗锯齿(aliasing),也就是反走样,来减少图像中锯齿的产生。在这之前,我们先要对原因进行一些分析

上图中有五个不同频率的函数,我们对它们使用相同的方式进行采样。可以发现采样点可以大致呈显出低频率的函数的图像,但随着函数频率的增高采样点恢复出来的函数图像与真实的函数图像的差距越来越大。通过这个例子我们可以知道,对于高频率的函数应该使用较高的采样率,低频率的函数应该使用较低的采样率

对上图的蓝色曲线进行采样,采样的结果是白色的采样点。而对黑色曲线进行采样,得到的也是相同的白色采样点。也就是说,使用同样的采样方法对两个函数进行采样,得到的结果无法区分,这种情况就称为走样

滤波就是去掉一系列频率,而傅里叶变换可以把函数变为不同频率的段。我们先可以使用傅里叶变换,将图像由时域变换到频域

上图中右侧就是左侧图像在频域上展开的结果(图像的中心为低频率,四周为高频率,使用亮度表示频率出现的情况;分析信号时会认为信号是一个无限重复的周期性信号,为了满足这个,我们把图片在水*方向上拼接无限多个,竖直方向也拼接无限多个;观察右图发现,出现较明显的竖杠是因为图片的拼接处产生了较强的变化。中心较亮说明这张图片集中在低频)

我们现在尝试开始滤波。我们先去除低频信号(高通滤波)

可以发现,去除低频信号后进行逆傅里叶变换得到的图像只显示出了一些边界(变化大的区域)

再去除高频信号看看(低通滤波)

可以发现去除高频信号之后,图像呈现出了模糊的效果

从上文看出,滤波就是去除掉一些特定的频率信息。而从另外的角度分析,滤波(filtering)=*均(averaging)=卷积(convolution)

上图就大致描述了卷积的过程,其实也就是加权*均。不过这个是在时域上,卷积具有对偶性

时域上的乘积等于频域上的卷积,反之亦然

就如这张图中所描述,我们可以取一个像素周围3*3的区域(上图中的表格为卷积核)进行卷积,也可以将原图和我们的卷积核都进行傅里叶变换并进行相乘,再进行逆傅里叶变换也得到了类似的效果

采样其实就是在重复频域或者频域上的内容

我们将a与c (冲激函数)相乘,得到的是一系列采样点。而前面得知时域上的相乘也对应了频域上的卷积。我们将a与c傅里叶变换得到的c和d图进行卷积操作,得到的就是f图。可以发现,采样是把原来的频谱给复制粘贴了很多

冲激函数在频域的间隔是时域的倒数。采样频率低,冲激函数时域宽,对应的频域就窄,会造成上图中信号重叠的现象,也就是说,信号发生了走样

增加采样率会使重叠情况减缓,也可以将高频位置的信号直接去除。也就是说,我们可以提高屏幕的分辨率,增加像素点的密集程度,可以减缓锯齿(走样)的产生,但这在大部分形况下不现实;我们选择使用滤波的方式(低通滤波)滤除高频信号。上文中可以看到,我们模糊的操作,类似于滤波滤除高频信号,类似于卷积

相比较于失真,丢失部分细节显得更好一些

所以介绍了这么多,我们得出结论,使用模糊的方式,来减少我们光栅化三角形时出现的走样问题。最直接的理解方式,就是对每一个像素进行*均

根据三角形覆盖像素小方块的面积大小,决定像素小方块的亮度,类似于*均

这个原理*似于接下来要介绍的抗锯齿方法:MSAA(Multisampling Antialiasing)

这个方法可以理解为将一个物理像素划分为多个小像素,在取每个小像素的中心为采样点。假设我们设置MSAA×4的话,那么每个物理像素就有了四个采样点。三角形对这些采样点覆盖情况就可以*似为对每一个物理像素的覆盖情况(点越多越精确),就达到了模糊的效果

至此,我们已经可以完成一个三角形的简单的光栅化和抗锯齿

下面是代码

由于老师的框架中函数传入的是三角形对象,所以先看三角形类的代码。

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;
    Vector2f tex_coords[3]; //texture u,v
    Vector3f normal[3]; //normal vector for each vertex

    //Texture *tex;
    Triangle();

    void setVertex(int ind, Vector3f ver); /*set i-th vertex coordinates */
    void setNormal(int ind, Vector3f n); /*set i-th vertex normal vector*/
    void setColor(int ind, float r, float g, float b); /*set i-th vertex color*/
    Vector3f getColor() const { return color[0]*255; } // Only one color per triangle.
    void setTexCoord(int ind, float s, float t); /*set i-th vertex texture coordinate*/
    std::array<Vector4f, 3> toVector4() const;
};
Triangle::Triangle() {
    v[0] << 0,0,0;
    v[1] << 0,0,0;
    v[2] << 0,0,0;

    color[0] << 0.0, 0.0, 0.0;
    color[1] << 0.0, 0.0, 0.0;
    color[2] << 0.0, 0.0, 0.0;

    tex_coords[0] << 0.0, 0.0;
    tex_coords[1] << 0.0, 0.0;
    tex_coords[2] << 0.0, 0.0;
}

void Triangle::setVertex(int ind, Vector3f ver){
    v[ind] = ver;
}
void Triangle::setNormal(int ind, Vector3f n){
    normal[ind] = n;
}
void Triangle::setColor(int ind, float r, float g, float b) {
    if((r<0.0) || (r>255.) ||
       (g<0.0) || (g>255.) ||
       (b<0.0) || (b>255.)) {
        fprintf(stderr, "ERROR! Invalid color values");
        fflush(stderr);
        exit(-1);
    }

    color[ind] = Vector3f((float)r/255.,(float)g/255.,(float)b/255.);
    return;
}
void Triangle::setTexCoord(int ind, float s, float t) {
    tex_coords[ind] = Vector2f(s,t);
}

std::array<Vector4f, 3> Triangle::toVector4() const
{
    std::array<Eigen::Vector4f, 3> res;
    std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) { return Eigen::Vector4f(vec.x(), vec.y(), vec.z(), 1.f); });
    return res;
}

类中封装并在类外实现了toVector4()函数,我们直接调用就能获取三角形的各个顶点坐标

void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    int min_x, max_x, min_y, max_y;
    min_x = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
    max_x = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
    min_y = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
    max_y = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
    //这里找出三角形的所在区域,避免不必要的遍历

    
    // TODO : Find out the bounding box of current triangle.
    //  iterate through the pixel and find if the current pixel is inside the triangle
   for (int x = min_x; x <= max_x; x++)
   {
       for (int y = min_y; y <= max_y; y++)
       {
           //判断像素中心点是否在连续三角形内,若在三角形内,就尝试对该像素进行着色,若深度测试通过,便着色
           if (insideTriangle(x + 0.5, y + 0.5, t.v))
           {
               // If so, use the following code to get the interpolated z value.
               auto tup = computeBarycentric2D((float)x + 0.5, (float)y + 0.5, t.v);
               float alpha;
               float beta;
               float gamma;
               std::tie(alpha, beta, gamma) = tup;
               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;
               //深度测试,通过便着色,并同时将深度存入缓存
               //这里有个细节之前没注意,就是buf的取值要用get_index函数
               if (depth_buf[get_index(x, y)] > z_interpolated)
               {
                   // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
                   //深度存入缓存
                   depth_buf[get_index(x, y)] = z_interpolated;
                   Vector3f point = { (float)x,(float)y,z_interpolated};
                   Vector3f color = t.getColor();
                   //着色
                   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.
}

insideTriangle函数在上文中已经提及。如果返回true,则证明这个点在三角形内,我们就进行深度测试(还未涉及,后面再讲),通过就进行着色

这样,我们就可以得到下面的图像

甚至不用放大就可以看到很多锯齿

接下来要进行抗锯齿。我们就使用刚才所提到的MSAA方法

int mass_w=2;
int mass_h=2;
const int max_count=mass_h*mass_w;
float pixel_step=1.0/mass_w;
float start_point=pixel_step/2.0;

//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    float min_x=v[0].x();
    float max_x=v[0].x();
    float min_y=v[0].y();
    float max_y=v[0].y();
    for(int i=0;i<3;i++){
        min_x=std::min(min_x,v[i].x());
        max_x=std::max(max_x,v[i].x());
        min_y=std::min(min_y,v[i].y());
        max_y=std::max(max_y,v[i].y());
    }//找到边界
   
    for(int x=min_x;x<max_x;x++){
        for(int y=min_y;y<max_y;y++){//per piexs
            float the_num=0.0f;
            for(float x_step=start_point;x_step<1;x_step+=pixel_step){
                for(float y_step=start_point;y_step<1;y_step+=pixel_step){
                    if(insideTriangle(x+x_step,y+y_step,t.v)){
                        the_num+=1.0f;//计算在三角形内部的像素点的数量
                    }
                }
            }
            if(the_num>0){//有像素分点在三角形内部
                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;
                int idx=get_index(x,y);
                if(z_interpolated<depth_buf[idx]){
                    Eigen::Vector3f p;
                    p<<x,y,z_interpolated;
                    if(the_num!=4)cout<<the_num<<endl;
                    set_pixel(p,t.getColor()*(the_num/max_count));
                    depth_buf[idx]=z_interpolated;
                }
            }
            
        }
    }
    // TODO : Find out the bounding box of current triangle.
    // iterate through the pixel and find if the current pixel is inside the triangle
 
    // 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;
 
    // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
}

得到下面的图像

可以看到锯齿现象得到了明显的改善,但是出现了我们不喜欢的黑边

产生这个的原因是因为原本位于上面的像素使用了抗锯齿,着色比例下降模糊后直接忽略了重叠的现象。

所以我们需要维护子像素的颜色,直接使用物理像素进行判定是不合理的。一般情况下,将子像素的颜色进行记录后取*均值即可

    for(int x=0;x<height;x++){
        for(int y=0;y<width;y++){
            //当前的像素的颜色,由子像素决定
            Eigen::Vector3f color={0.0,0.0,0.0};
            for(float x_s=start_point;x_s<1.0f;x_s+=pixel_step){
                for(float y_s=start_point;y_s<1.0f;y_s+=pixel_step){
                    int idx=get_index_ssaa(x,y,x_s,y_s);
                    color+=frame_buf_ssaa[idx];
                }
            }
            Eigen::Vector3f p;
            p<<x,y,0.0f;//设置最*的距离0
            set_pixel(p,color/(max_count));//设置*均颜色
        }
    }

这时我们可以发现,成功去除了黑边

有一个问题,两个三角形的顺序似乎搞反了

发现问题在于深度数组初始化为无穷大。我们的视角假设是向-z的方向观察的,而我们进行深度检测时设置的值小的覆盖,这导致我们的图像重叠顺序反了,更改为无穷小和大的覆盖即可

posted @   haihaichibaola  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
点击右上角即可分享
微信分享提示