【GAMES101】作业2 Triangles and Z-buffering

一.作业描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二.作业解析

1. 测试点是否在三角形

测试一个点是否在三角形中,这里我尝试了2种方法。课上讲的是同侧法。

a.面积法

面积法的原理:
在这里插入图片描述

我们知道叉乘的结果是个向量,方向在z轴上,对于二维空间的叉乘:
V1(x1, y1) X V2(x2, y2) = x1y2 -y1x2
对于两个向量A,B,如果暂时忽略他们叉乘结果的方向,则有:
A x B = |A||B|Sin(θ)
即叉积的绝对值就是以A,B为两边形成的平行四边形的面积,所以求面积经常用到向量的叉积,要注意记得加上绝对值,因为叉积可能为负值。

下面是面积法的代码,写的比较笨…

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]
    //v是点坐标
    Vector2f v[3];
    for(int i=0;i<3;i++){
        v[i]={_v[i].x(),_v[i].y()};
    }
    //算三条边向量
    Vector2f f0,f1,f2;
    f0 = {v[0].x()-v[1].x(),v[0].y()-v[1].y()};
    f1 = {v[1].x()-v[2].x(),v[1].y()-v[2].y()};
    f2 = {v[2].x()-v[0].x(),v[2].y()-v[0].y()};
    //按照叉积算面积
    double s=fabs(f0[0]*f1[1]-f0[1]*f1[0])/2.0;
    double s1,s2,s3;
    s1=fabs((v[0].x()-x)*f0[1]-f0[0]*(v[0].y()-y))/2.0;
    s2=fabs((v[1].x()-x)*f1[1]-f1[0]*(v[1].y()-y))/2.0;
    s3=fabs((v[2].x()-x)*f2[1]-f2[0]*(v[2].y()-y))/2.0;
	//因为是浮点数,判断阈值设置不好会影响栅格化结果
    if(fabs(s-s1-s2-s3)<0.01)
        return true;
    return false;
}

估计是因为面积法涉及到浮点数double相减的问题,如果函数最后的判断阈值设置的不好(过小)就可能会出现下面的情况。
在这里插入图片描述
把阈值这里稍微改大一点,判断就恢复正常了。
在这里插入图片描述

b.同侧法

面积法的弊端还挺明显的,现在来看一看同侧法。
同侧法的原理:
同侧法的原理是如果一个点在三角形三条边的同侧,那么这个点就在三角形中。
判断点是否在三角形边的同侧,也需要用到叉乘。叉乘在图形学中是判断左和右的:
用a叉乘b,得到的结果是正的,说明b在a的左侧;
用b叉乘a,得到的结果是负的,说明b在a的右侧。

思路大概为:
1,求出三个向量MA,MB,MC.
2,算MA X MB,MB X MC,MC X MA (X表叉乘)
3,如果此三组的向量叉乘的结果都是同号的(或都正,或都负),即方向相同的,则说明点M在三角形每条边的同侧,即内部。否则必在外部。

代码如下:

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]
    //将三角形的三点坐标存入v中
    Vector3f v[3];
    for(int i=0;i<3;i++){
        v[i]={_v[i].x(),_v[i].y(),1.0};
    }
    //求出三个向量MA,MB,MC 
    Vector3f f0,f1,f2;
    f0 = v[1]-v[0];
    f1 = v[2]-v[1];
    f2 = v[0]-v[2];
    Vector3f p(x,y,1.);
    //算MA X MB,MB X MC,MC X MA 并判断
    if(((f0.cross(p-v[1])).z()>0) && ((f1.cross(p-v[2])).z()>0) && ((f2.cross(p-v[0])).z()>0))
        return true;
    return false;
}

2. 多采样反走样MSAA

MSAA的做法是计算在一个像素中有几个采样点会被三角形覆盖到,计算颜色的时候只会利用像素中心坐标计算一次颜色(即所有的信息都会被插值到像素中心然后取计算颜色),
然后将该像素中心计算出来的颜色值乘以三角形覆盖的采样点占比即可,这样大大减少了计算量,并且得到反走样效果也是很不错的。

Z-Buffer算法

空间中的物体投影到二维屏幕平面上,对于屏幕上的每个像素点所对应的可能不止一个三角形面上的点,此时我们应该选择离摄像头最近的像素点显示在屏幕上。
这里便要利用到我们之前做model−>view−>projection变换之后所得到的深度值 z ,作业里面提到了定义z越大离摄像机越远。
在这里插入图片描述

Z-Buffer算法需要为每个像素点维持一个深度数组记为zbuffer,其每个位置初始值置为无穷大(即离摄像机无穷远)。
在这里插入图片描述

遍历每个三角形面上的每一个像素点[x,y],如果该像素点的深度值z,小于zbuffer[x,y]中的值,则更新zbuffer[x,y]值为该点深度值z,并同时更新该像素点[x,y]的颜色为该三角形面上的该点的颜色。

重心坐标插值(矫正透视)

在这里插入图片描述
通过给定合法的α , β , γ ,即可通过三个顶点对三角形内部任何一个点进行插值。
下面直接展示重心坐标参数α , β , γ推导计算的结果:在这里插入图片描述
在这里插入图片描述

其中作业里自带的ComputeBarycentric2D函数就是封装好的求重心坐标参数的函数。

//x,y  屏幕上要判断的点的坐标(计算像素点的中点,像素点差值为1)
//v  三角形顶点坐标数组,[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]
//c1,c2,c3	重心坐标参数[α,β,γ]

static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v)
{
    float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y());
    float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y());
    float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y());
    return {c1,c2,c3};
}

先前仅是对两个三角形的三个顶点进行了投影变换,之后才在这三个顶点内部进行插值着色。虽然最后的光栅化步骤仅在2D平面内进行,理论上只需三个顶点的(x,y)值便能完成插值计算,但是为了实现z-buffer,我们没有将原顶点的z值丢弃,而是将它们一齐进行了投影变换,并被用来插值三角形内部点的深度值,但这会带来一个问题。

重心坐标并不具备变换一致性,也即,经过变换后的三个顶点的重心坐标和变换前的对应原顶点的重心坐标并不相同,这会导致,变换后的顶点插值出来的内部点的z深度会和变换前的顶点的深度插值结果不一致
而用来进行遮挡判断的z值应该是变换前的原值,所以在光栅化这一步骤,我们不应计算变换后的插值z,而是通过当前插值点算出其变换前的z值

如下图所示,空间存在一定的透视关系,s和t的值是不一样的。
要绘制出空间中的物体在屏幕上的投影,需要先计算出s与t 的关系。
这样就可以正确的利用屏幕空间的系数s插值到正确的view space的结果,让实际物体的点正确的对应到屏幕显示的点上。

定义屏幕空间的比例为s,view space中为t:
在这里插入图片描述

根据空间中三角形相似特性和线性插值联立可以求得s与t的关系。
在这里插入图片描述
推导得到空间中的线性插值矫正的结果:
(详细过程在链接: 重心坐标插值.)
在这里插入图片描述
对于重心坐标插值,s对应Z2,1-s对应Z1,类似推导得出 α对应ZA , β对应ZB , γ对应ZC:
在这里插入图片描述
对于任意属性I,按照线性插值为例进行推导:
请添加图片描述

最后得到的重心坐标任意属性插值如下:
在这里插入图片描述

float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());

作业里面的w_reciprocal对应Zt,也就是利用屏幕空间的重心坐标插值得出正确的view space下的z值,但是作业框架里面把深度值(这个深度值和z值在作业框架里面不是一个东西)当成了一种属性,所以最终求深度值的式子你可以对应文中的16式来看,是一模一样的。
顶点的w的值是实际的观察空间下的z值,这是由于perspective矩阵造成的
perspective矩阵,会把观察空间中的z变到剪裁空间中的w。

        Triangle t;
        Eigen::Vector4f v[] = {
                mvp * to_vec4(buf[i[0]], 1.0f),
                mvp * to_vec4(buf[i[1]], 1.0f),
                mvp * to_vec4(buf[i[2]], 1.0f)
        };
        //Homogeneous division
        for (auto& vec : v) {
            vec /= vec.w();
        }
        //Viewport transformation 视口变换
        for (auto & vert : v)
        {
            vert.x() = 0.5*width*(vert.x()+1.0);
            vert.y() = 0.5*height*(vert.y()+1.0);
            vert.z() = vert.z() * f1 + f2;
        }

对于透视矫正看了很多博客一直都是似懂非懂的感觉,在一篇博客下面看到了别人的评论:
在这里插入图片描述

3. MSAA黑边解决方案

rasterize_triangle的代码如下:

void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    BoundingBox bbox;
    bbox.xmin = std::min(v[0][0],std::min(v[1][0],v[2][0]));
    bbox.xmax = std::max(v[0][0],std::max(v[1][0],v[2][0]));
    bbox.ymin = std::min(v[0][1],std::min(v[1][1],v[2][1]));
    bbox.ymax = std::max(v[0][1],std::max(v[1][1],v[2][1]));
    bbox.xmin = (int)floor(bbox.xmin);
    bbox.xmax = (int)ceil(bbox.xmax);
    bbox.ymin = (int)floor(bbox.ymin);
    bbox.ymax = (int)ceil(bbox.ymax);
	bool MSAA = true;
	//MSAA 4X
	if (MSAA) {
		// 格子里的细分四个小点坐标
		std::vector<Eigen::Vector2f> pos
		{
			{0.25,0.25},
			{0.75,0.25},
			{0.25,0.75},
			{0.75,0.75},
		};

        for(int x=bbox.xmin;x<=bbox.xmax;x++){
            for(int y=bbox.ymin;y<=bbox.ymax;y++){
                float minDepth = FLT_MAX;
                // 四个小点中落入三角形中的点的个数
                int count=0;
                for(int i=0;i<4;i++){
                    if (insideTriangle((float)x + pos[i][0], (float)y + pos[i][1], t.v)) {
                        auto [alpha,  beta, gamma] = computeBarycentric2D((float)x + pos[i][0], (float)y + pos[i][1], 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;
                        // 记录最小深度
                        minDepth = std::min(minDepth, z_interpolated);
                        count++;
                    }
                }
                if (count != 0) { //如果有采样点被三角形覆盖
                //将其位置处的插值深度值与深度缓冲区 中的相应值进行比较
					if (depth_buf[get_index(x, y)] > minDepth) {
						Vector3f color = t.getColor() * count / 4.0;
						Vector3f point(3);
						point << (float)x, (float)y, minDepth;
						// 替换深度
						depth_buf[get_index(x, y)] = minDepth;
						// 修改颜色
						set_pixel(point, color);
					}
				}
            }
        }

    }
    else { //不进行超采样
		for (int x = bbox.xmin; x <= bbox.xmax; x++) {
			for (int y = bbox.ymin; y <= bbox.ymax; y++) {
                //下个采样点是否在三角形内
				if (insideTriangle((float)x + 0.5, (float)y + 0.5, t.v)) {
					auto [alpha,  beta, gamma] = computeBarycentric2D((float)x + 0.5, (float)y + 0.5, 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;

					if (depth_buf[get_index(x, y)] > z_interpolated) {
						Vector3f color = t.getColor();
						Vector3f point(3);
						point << (float)x, (float)y, z_interpolated;
						depth_buf[get_index(x, y)] = z_interpolated;
						set_pixel(point, color);
					}
				}
			}
		}
	}

}

比较卑微的是按照上面的代码来做,一开MSAA就出现了黑边:
在这里插入图片描述
参照MSAA黑边的解决方案:
在这里插入图片描述
看完上面的博客我大概知道了黑边的出现原因,就是说渲染是从离镜头近到镜头远的方向进行的,且上面的代码设置颜色代码代码t.getColor()*count/4.0得到的是很深的颜色,像黑色。

我看到别人的解决方案有改颜色设置部分的代码,也有改渲染顺序为从远到近的,然而我单独改任何一地方都没有消除黑边,最后我同时改了这两点↓,解决了问题。

在这里插入图片描述
在这里插入图片描述
成功搞定!
在这里插入图片描述

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值