手搓一个小型渲染器(三)

深度缓存(Z buffer)

理论上我们可以画出所有的三角形,而不丢弃任何三角形。如果我们按照正确的顺序绘制三角形(从后往前)。那么前面的将会遮挡住后面的部分,这称为画家算法。

一个简单场景的渲染

假设现在有一个场景中包含了三个三角形,摄像机将从上往下看过去,我们将彩色三角形投影到白色屏幕上。

img

渲染结果如下:

img

如果现在我们提问,蓝色三角形在红色三角形之前还是之后,我们无法作答,这时画家算法不起作用,我们当然可以将三角形一分为二,一半在红色前面,一半在红色后面,但是对于大量的三角形,这会造成很大的计算资源开销。

一种更简单的方法:Y-buffer

我们首先对场景进行降维,仅考虑黄色切面的渲染情况。

img

现在在白色屏幕渲染得到的结果应该一条线段(即单排的像素点)。

img

现在我们的场景已经降维成二维,因此我们可以用line()函数简单的绘制出场景,场景的侧面图如下所示:

{
	TGAImage scene(width, height, TGAImage::RGB);
    // scene "2d mesh"
    line(Vec2i(20, 34), Vec2i(744, 400), scene, red);
    line(Vec2i(120, 434), Vec2i(444, 400), scene, green);
    line(Vec2i(330, 463), Vec2i(594, 200), scene, blue);
    // 投影面
    line(Vec2i(10, 10), Vec2i(790, 10), scene, white);
    scene.write_tga_file("scene.tga");
}

场景的侧视图如下所示:
在这里插入图片描述
对于这个场景,我们需要定义一个y-buffer,用于存储最上方的线段颜色,具体代码如下:

void yBuffer(Vec2i v0, Vec2i v1, int* buffer, TGAImage& image, TGAColor color) {
    //从x小的开始
    if (v0.x > v1.x)
        std::swap(v0, v1);

    for (int i = v0.x; i <= v1.x; i++) {
        //遍历每一段线条,
        float t = (i - v0.x) / float(v1.x - v0.x);
        int y = (1.- t) * v0.y + t * v1.y;
        if (buffer[i] < y) {
            buffer[i] = y;
            image.set(i, 0, color);
        }   
    }
}

运行代码如下:

void yBuffer(Vec2i v0, Vec2i v1, int* buffer, TGAImage& image, TGAColor color) {
    //从x小的开始
    if (v0.x > v1.x)
        std::swap(v0, v1);

    for (int i = v0.x; i <= v1.x; i++) {
        float t = (i - v0.x) / float(v1.x - v0.x);
        int y = (1.- t) * v0.y + t * v1.y;

        if (buffer[i] < y) {
            buffer[i] = y;
            image.set(i, 0, color);
        }   
    }
}

int main() {
    TGAImage scene(width, height, TGAImage::RGB);
    // scene "2d mesh"
    line(Vec2i(20, 34), Vec2i(744, 400), scene, red);
    line(Vec2i(120, 434), Vec2i(444, 400), scene, green);
    line(Vec2i(330, 463), Vec2i(594, 200), scene, blue);
    // screen line
    line(Vec2i(10, 10), Vec2i(790, 10), scene, white);
    scene.write_tga_file("scene.tga");

    TGAImage render(width, 16, TGAImage::RGB);
    int ybuffer[width]{};
    for (int i = 0; i < width; i++)
        ybuffer[i] = INT_MIN;

    yBuffer(Vec2i(20, 34), Vec2i(744, 400), ybuffer, render, red);
    yBuffer(Vec2i(120, 434), Vec2i(444, 400), ybuffer, render, green);
    yBuffer(Vec2i(330, 463), Vec2i(594, 200), ybuffer, render, blue);

    //只有一行像素很难看清楚,复制几行显示清晰一些
    for (int i = 0; i < width; i++)
        for (int j = 0; j < 16; j++)
            render.set(i, j, render.get(i, 0)); 

    render.write_tga_file("render.tga");
}

迭代v0.x和v1.x之间的所有x坐标,并计算线段的相应y坐标。然后我检查我们在数组ybuffer中得到的当前x索引。如果当前的y值比ybuffer中的值更接近相机,那么我将其绘制在屏幕上并更新ybuffer。
在这里插入图片描述

绘制到3D平面

对于3D平面我们也可以用同样的方式建立一个z-buffer,由于在三维空间,那么z-buffer的长度应该为屏幕的面积,即:

int *zbuffer = new int [width * height];

这里我们将二维的zbuffer展平成一维,zbuffer对应的索引值与平面的坐标值对应关系如下:

int x = index % width;
int y = index / width;

然后我们将遍历所有三角形,更新zbuffer,现在唯一困难的点在于我们怎么确定z值呢,在二维中,我们使用下面的方式:

  int y = v0.y*(1.-t) + v1.y*t;

那么对于二维线段来说,t就是线段上的重心坐标,那么在三角形中,同样的也是重心坐标,我们的想法是采用三角形光栅化的重心坐标版本,对于我们想要绘制的每个像素,只需将其重心坐标乘以我们光栅化的三角形顶点的z值。代码如下:

我们将画线、渲染三角形以及判断重心坐标点代码抽离出来得到line.cpp

#include "line.h"

/*
*    绘制两点间的直线
*/
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
    bool steep = false;
    if (std::abs(x0 - x1) < std::abs(y0 - y1)) {
        std::swap(x0, y0);
        std::swap(x1, y1);
        steep = true;
    }
    if (x0 > x1) {
        std::swap(x0, x1);
        std::swap(y0, y1);
    }
    int dx = x1 - x0;
    int dy = y1 - y0;
    int derror2 = std::abs(dy) * 2;
    int error2 = 0;
    int y = y0;
    for (int x = x0; x <= x1; x++) {
        if (steep) {
            image.set(y, x, color);
        }
        else {
            image.set(x, y, color);
        }
        error2 += derror2;
        if (error2 > dx) {
            y += (y1 > y0 ? 1 : -1);
            error2 -= dx * 2;
        }
    }
}

/*
*    重载line的Vec2i版本
*/
void line(Vec2i v0, Vec2i v1, TGAImage& image, TGAColor color)
{
    line(v0.x, v0.y, v1.x, v1.y, image, color);
}

/*
*     获得三角形的重心坐标
*/
Vec3f barycentric(Vec2i* pts, Vec2i P) {
    //uAB + vAC + PA = 0
    //将上述向量的x分量和y分量拆开,并且做叉积,可以得到[u, v, 1](还需要归一化)
    Vec3f u = Vec3f(pts[1].x - pts[0].x, pts[2].x - pts[0].x, pts[0].x - P.x) ^ Vec3f(pts[1].y - pts[0].y, pts[2].y - pts[0].y, pts[0].y - P.y);
    if (std::abs(u.z) < 1) return Vec3f(-1, 1, 1);

    //返回[u,v,1]
    return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
}

//改进的三角形渲染算法
void drawTriangle(Vec2i* pts, TGAImage& image, TGAColor color)
{
    //确定三角形的包围框
    Vec2i bboxmin{ image.height() - 1, image.width() - 1 };
    Vec2i bboxmax{ 0 ,0 };
    Vec2i bboxclamp{ image.height() - 1, image.width() - 1 };

    //遍历三角形顶点,确定四边形边框
    for (int i = 0; i < 3; i++)
    {
        bboxmin.x = std::max(0, std::min(bboxmin.x, pts[i].x));
        bboxmin.y = std::max(0, std::min(bboxmin.y, pts[i].y));

        bboxmax.x = std::max(0, std::max(bboxmax.x, pts[i].x));
        bboxmax.y = std::max(0, std::max(bboxmax.y, pts[i].y));
    }

    //遍历框内像素,确定像素点是否在三角形内部(使用重心坐标判断)
    for (int i = bboxmin.x; i <= bboxmax.x; i++) {
        for (int j = bboxmin.y; j <= bboxmax.y; j++) {
            Vec3f bc_screen = barycentric(pts, { i,j });
            if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
            image.set(i, j, color);
        }
    }
}


Vec3f barycentric(Vec3f* pts, Vec3f P) {
    //uAB + vAC + PA = 0
    //将上述向量的x分量和y分量拆开,并且做叉积,可以得到[u, v, 1](还需要归一化)
    Vec3f u = Vec3f(pts[2].x - pts[0].x, pts[1].x - pts[0].x, pts[0].x - P.x) ^ Vec3f(pts[2].y - pts[0].y, pts[1].y - pts[0].y, pts[0].y - P.y);
    //注意这里变成了float
    if (std::abs(u.z) <= 1e-2) return Vec3f(-1, 1, 1);

    //返回[u,v,1]
    return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
}

/*
*   带z-buffer的三角形渲染算法
*/
void drawTriangle(Vec3f* pts, float* zbuffer, TGAImage& image, TGAColor color)
{
    //确定三角形的包围框
    Vec2f bboxmin { std::numeric_limits<float>::max(), std::numeric_limits<float>::max() };
    Vec2f bboxmax { std::numeric_limits<float>::min(), std::numeric_limits<float>::min() };
    Vec2f bboxclamp{ float(image.width() - 1), float(image.height() - 1) };

    //遍历三角形顶点,确定四边形边框
    for (int i = 0; i < 3; i++)
    {
        bboxmin.x = std::max(0.f, std::min(bboxmin.x, pts[i].x));
        bboxmin.y = std::max(0.f, std::min(bboxmin.y, pts[i].y));

        bboxmax.x = std::min(bboxclamp.x, std::max(bboxmax.x, pts[i].x));
        bboxmax.y = std::min(bboxclamp.y, std::max(bboxmax.y, pts[i].y));
    }

    //遍历框内像素,确定像素点是否在三角形内部(使用重心坐标判断)
    Vec3f p{};
    for (p.x = bboxmin.x; p.x <= bboxmax.x; p.x++) {
        for (p.y = bboxmin.y; p.y <= bboxmax.y; p.y++) {
            Vec3f bc_screen = barycentric(pts, p);
            //Vec3f bc_screen = barycentric(pts[0], pts[1], pts[2], p);
            if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
            //利用重心坐标计算三角形内点p的z坐标值
            p.z = 0;
            for (int i = 0; i < 3; i++) p.z += pts[i].z * bc_screen[i];
            //更新z-buffer,并且只有更新的时候,需要渲染更近的像素颜色值
            if (zbuffer[int(p.x * 800 + p.y)] < p.z) {
                zbuffer[int(p.x * 800 + p.y)] = p.z;
                image.set(p.x, p.y, color);
            }            
        }
    }
}

main.cpp

#include "line.h"
#include "model.h"
#include "TGAImage.h"


int main() {
    int width = 800;
    int height = 800;

    TGAImage image(width, height, TGAImage::RGB);

    Vec3f lightVec{ 0, 0, 1 };

    Model* head = new Model("./obj/african_head/african_head.obj");

    //zbuffer的初始化也会对渲染结果产生影响,这里不能使用FLT_MIN
    /*float* zbuffer = new float[width * height];
    for (int i = width * height; i--; zbuffer[i] = FLT_MIN);*/
    float* zbuffer = new float[width * height];
    for (int i = width * height; i--; zbuffer[i] = -std::numeric_limits<float>::max());

    for (int i = 0; i < head->nfaces(); i++)
    {
        std::vector<int> face = head->face(i);

        //顶点世界坐标
        Vec3f world_coords[3]{};
        //顶点屏幕坐标
        Vec3f screen_coords[3]{};

        for (int j = 0; j < 3; j++) {
            world_coords[j] = head->vert(face[j]);
            screen_coords[j] = Vec3f(int((world_coords[j].x + 1.) * width / 2.), int((world_coords[j].y + 1.) * height / 2.), world_coords[j].z);
        }

        //三角形法线向量,三角形两边叉乘得到法线向量
        Vec3f n = (world_coords[0] - world_coords[1]) ^ (world_coords[1] - world_coords[2]);
        n.normalize();
        float intensity = lightVec * n;

        Vec3f* p_coords = &screen_coords[0];

        drawTriangle(p_coords, zbuffer, image, TGAColor{ std::uint8_t(intensity * 255), std::uint8_t(intensity * 255), std::uint8_t(intensity * 255), 255 });
    }

    image.write_tga_file("render_result.tga");
    delete head;
}

渲染结果:
在这里插入图片描述

添加纹理

添加纹理的过程非常简单,我们读取obj文件,其中vt行代表每个三角形顶点对应二维纹理的坐标,在三角形渲染过程中,我们可以直接使用重心坐标,三角形内部的点可以看作三个顶点纹理插值的结果。代码如下:

/*
*   带z-buffer,以及纹理的三角形渲染算法
*/
void drawTriangle(Vec3f* pts, Vec2f* tex_coords, float* zbuffer, TGAImage& image, TGAImage& texture)
{
    //确定三角形的包围框
    Vec2f bboxmin{ std::numeric_limits<float>::max(), std::numeric_limits<float>::max() };
    Vec2f bboxmax{ std::numeric_limits<float>::min(), std::numeric_limits<float>::min() };
    Vec2f bboxclamp{ float(image.width() - 1), float(image.height() - 1) };

    //遍历三角形顶点,确定四边形边框
    for (int i = 0; i < 3; i++)
    {
        bboxmin.x = std::max(0.f, std::min(bboxmin.x, pts[i].x));
        bboxmin.y = std::max(0.f, std::min(bboxmin.y, pts[i].y));

        bboxmax.x = std::min(bboxclamp.x, std::max(bboxmax.x, pts[i].x));
        bboxmax.y = std::min(bboxclamp.y, std::max(bboxmax.y, pts[i].y));
    }

    //遍历框内像素,确定像素点是否在三角形内部(使用重心坐标判断)
    Vec3f p{};
    for (p.x = bboxmin.x; p.x <= bboxmax.x; p.x++) {
        for (p.y = bboxmin.y; p.y <= bboxmax.y; p.y++) {
            Vec3f bc_screen = barycentric(pts, p);
            //Vec3f bc_screen = barycentric(pts[0], pts[1], pts[2], p);
            if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
            //利用重心坐标计算三角形内点p的z坐标值
            p.z = 0;
            for (int i = 0; i < 3; i++) p.z += pts[i].z * bc_screen[i];
            
            //插值计算纹理坐标
            Vec2f p_tex{};
            for (int i = 0; i < 3; i++) {
                p_tex.x += tex_coords[i].x * bc_screen[i] * texture.height();
                p_tex.y += tex_coords[i].y * bc_screen[i] * texture.width();
            }

            TGAColor color = texture.get(p_tex.x, p_tex.y);
            //更新z-buffer,并且只有更新的时候,需要渲染更近的像素颜色值
            if (zbuffer[int(p.x * 800 + p.y)] < p.z) {
                zbuffer[int(p.x * 800 + p.y)] = p.z;
                image.set(p.x, p.y, color);
            }
        }
    }
}
#include "line.h"
#include "model.h"
#include "TGAImage.h"


int main() {
    int width = 800;
    int height = 800;

    TGAImage image(width, height, TGAImage::RGB);
    TGAImage TextureImage;
    //纹理图像
    TextureImage.read_tga_file("./obj/african_head/african_head_diffuse.tga");
    Model* head = new Model("./obj/african_head/african_head.obj");

    //zbuffer的初始化也会对渲染结果产生影响,这里不能使用FLT_MIN
    float* zbuffer = new float[width * height];
    for (int i = width * height; i--; zbuffer[i] = -std::numeric_limits<float>::max());

    for (int i = 0; i < head->nfaces(); i++)
    {
        std::vector<int> face = head->face(i);
        std::vector<int> face_texture = head->f_texture(i);

        //顶点世界坐标
        Vec3f world_coords[3]{};
        //顶点屏幕坐标
        Vec3f screen_coords[3]{};

        Vec2f texture[3]{};

        for (int j = 0; j < 3; j++) {
            world_coords[j] = head->vert(face[j]);
            screen_coords[j] = Vec3f(int((world_coords[j].x + 1.) * width / 2.), int((world_coords[j].y + 1.) * height / 2.), world_coords[j].z);
            texture[j] = head->texture(face_texture[j]);
        }

        //三角形法线向量,三角形两边叉乘得到法线向量
        Vec3f n = (world_coords[0] - world_coords[1]) ^ (world_coords[1] - world_coords[2]);

        Vec3f* p_coords = &screen_coords[0];
        TGAColor TexColor;
        drawTriangle(p_coords, texture, zbuffer, image, TextureImage);
    }

    image.write_tga_file("render_result_texture2.tga");
    delete head;
}

得到结果如下:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值