返回
Featured image of post 从零开始的软渲染器 Z-Buffer

从零开始的软渲染器 Z-Buffer

导航页面

Z-Buffer介绍

首先说说为什么要引入Z-Buffer,在我们之前的光栅化操作中,我们完全没有管三角形的前后关系

拿这张图来举例,假设我们的摄像机在\(S\)点,则我们看到的\(ABC\)则会完全遮盖住\(abc\),但是,我们在光栅化的时候没有考虑到距离的因素。实际上我们绘制在屏幕上的三角形取决于绘制的顺序,如果我们先绘制\(ABC\)再绘制\(abc\),则我们只会看到\(abc\)。这还是简单的情况,复杂的情况还有三角形相交等等。

为了引入距离以及遮挡关系,我们使用Z-Buffer算法。

把摄像机对准的方向当作\(z\)轴负半轴,则所有点的\(z\)轴坐标也就确定了,显然的,\(z\)坐标大的点在前面,遮挡了\(z\)坐标小的物体,我们绘制三角形的时候,检测这个像素点的\(z\)坐标最大是多少,比它大,则绘制该像素,并且更新最大值,否则不绘制。

注意这里的\(z\)轴坐标是透视投影之后的坐标。

具体实现

话不多说我们直接上代码

bool ras::trianglePhong(
    TGAImage & image,
    geo::TriCoords const & tcoords,
    std::array<geo::OARColorf, 3> const & colors,
    geo::vec4f const & lightPos, // 光的位置
    geo::OARColorf const & light, // 光的颜色
    geo::vec4f const & cameraPos, //相机的位置
    float zbuffer[]
){
    float const EPS = 1e-5;
    int width = image.getWidth(), height = image.getHeight();
    std::array<geo::vec2i,3> screenPoints;

    for(int i=0;i<3;i++){
        screenPoints[i][0] = (int)(tcoords.screenCoords[i][0] / tcoords.screenCoords[i].w+.5f);
        screenPoints[i][1] = (int)(tcoords.screenCoords[i][1] / tcoords.screenCoords[i].w+.5f);
    }

    int maxx = 0, minx = width-1, maxy = 0, miny = height-1;
    for(int i=0;i<3;i++){
        maxx = std::max(maxx, screenPoints[i].x);
        minx = std::min(minx, screenPoints[i].x);
        maxy = std::max(maxy, screenPoints[i].y);
        miny = std::min(miny, screenPoints[i].y);
    }
    maxx = std::min(maxx, width-1);
    minx = std::max(minx, 0);
    maxy = std::min(maxy, height-1);
    miny = std::max(miny, 0);

    geo::vec4f kd = {.75f, .75f, .75f, 1.f};
    geo::vec4f ks = {1.f, 1.f, 1.f, 1.f};
    geo::vec4f ka = {.2f, .2f, .2f, 1.f}; //设置三种反射的反射因子

    for(int x=minx;x<=maxx;x++){
        for(int y=miny;y<=maxy;y++){
            std::tuple<float,float,float> ret = geo::getBarycentric(screenPoints, geo::vec2i(x,y));
            float alpha = std::get<0> (ret);
            float beta  = std::get<1> (ret);
            float gamma = std::get<2> (ret);
            if(alpha<-EPS || beta<-EPS || gamma<-EPS) continue;

            float z = alpha * tcoords.screenCoords[0].z +
                      beta  * tcoords.screenCoords[1].z +
                      gamma * tcoords.screenCoords[2].z;
            // z值在这里可以先这样用,下一节我们会发现这样插值是有误差的
            // 虽然我们这里传进来的是屏幕空间而非剪裁空间,但这两个空间的z坐标是相同的

            if(z<zbuffer[y*width+x]){
                continue;
            }
            zbuffer[y*width+x] = z;

            geo::vec4f norm = alpha*tcoords.norms[0] + beta*tcoords.norms[1] + gamma*tcoords.norms[2];
            geo::vec4f world =  alpha*tcoords.worldCoords[0] + beta*tcoords.worldCoords[1] + gamma*tcoords.worldCoords[2];
            // 插值得出三角形内点的世界坐标与法向量

            geo::vec4f l = geo::normalized(lightPos-world); //顶点到光源的单位向量
            geo::vec4f v = geo::normalized(cameraPos-world); //顶点到相机的单位向量
            geo::vec4f h = geo::normalized(v+l); //半程向量
            geo::normalize(norm); // 不进行单位化,specular就会过强

            geo::OARColorf ld = kd * light * std::max(0.f, geo::dot(l, norm));
            geo::OARColorf ls = ks * light * std::pow(std::max(0.f,geo::dot(norm, h)), 100.f);
            geo::OARColorf la = ka * geo::vec4f(.3f, .3f, .3f, 1.f); //三种光强
            geo::vec4f intensity = ld + ls + la;

            geo::OARColor color = geo::toOARColor(
                (alpha * colors[0] + beta  * colors[1] + gamma * colors[2]) * intensity
            );
            // 最终物体的颜色是 自身颜色*光照颜色
            image.setFragment(x,y,color);
        }
    }

    return true;
}

需要注意的是,我们这里直接用线性的重心坐标插值去计算三角形内部像素的\(z\)轴坐标了。虽然这里这样用确实效果还可以,但是下一节我们将会知道这样的差值是有误差的。

使用例

我们继续使用teapot模型,代码见 这里

现在观察壶嘴连接处,已经不会再有穿模的现象了。