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

文章讲述了如何使用扫线算法进行三角形边框的绘制,然后通过优化实现三角形内部的填充,引入了重心坐标来判断像素是否在三角形内,并展示了如何添加光照效果和实现背面剔除,以提高图形渲染的效率和真实性。
摘要由CSDN通过智能技术生成

三角光栅化和背面剔除

绘制三角形的最简单的方法,扫线:

#include <iostream>
#include "model.h"
#include "tgaimage.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;
        }
    }
}

void line(Vec2i v0, Vec2i v1, TGAImage& image, TGAColor color)
{
    line(v0.x, v0.y, v1.x, v1.y, image, color);
}

void drawTriangle(Vec2i v0, Vec2i v1, Vec2i v2, TGAImage& image, TGAColor color)
{
    line(v0, v1, image, color);
    line(v1, v2, image, color);
    line(v2, v0, image, color);
}

int main() {
   
    Vec2i v0{ 200, 200 };
    Vec2i v1{ 500, 500 };
    Vec2i v2{ 200, 400 };

	int width = 800;
	int height = 800;

	TGAColor white{255, 0, 0, 255 };
	TGAImage image(width, height, TGAImage::RGB);

    drawTriangle(v0, v1, v2, image, white);
    image.write_tga_file("Triangle.tga");
}

最简单的三角形绘制方法就是使用扫线,将点两两绘制直线,最终得到三角形,结果如下:
在这里插入图片描述
现在我们得到了三角形边框的绘制,但是我们如何填充这个三角形呢?

在这之前,我们需要讨论绘制一个绘制三角形方法所需要的特征:

  • 它应该是简单而高效的
  • 它应该具有对称性,最终得到的图像不会随着输入顶点的顺序改变而改变
  • 如果两个三角形有共同的两个顶点,那么这两个三角形之间将没有空隙
  • 我们还可以提出更多的需求,而下面的则是在传统扫描线中经常使用的:
    • 根据三角形的y坐标对顶点进行排序
    • 同时对三角形的左侧和右侧进行光栅化
    • 在左右边界点之间绘制一条水平线段。

上面的需求中,我们需要对三角形的y坐标进行排序,因此我们可以在drawTriangle函数中进行排序,然后将顺序从小到大排序

void drawTriangle(Vec2i v0, Vec2i v1, Vec2i v2, TGAImage& image, TGAColor color)
{
	if (v0.y > v1.y) std::swap(v0, v1);
    if (v0.y > v2.y) std::swap(v0, v2);
    if (v1.y > v2.y) std::swap(v1, v2);

    line(v0, v1, image, color);
    line(v1, v2, image, color);
    line(v2, v0, image, color);
}

在对y坐标排序前,输入顺序将会影响三角形的绘制,结果如下:
在这里插入图片描述

通过对输入顶点的y大小进行排序,这样我们就完成了绘制时的对称性,即输入顶点的顺序并不会影响最终绘制三角形的结果
在这里插入图片描述

现在我们已经完成了绘制三角形边框的对称性,接下来我们需要考虑的就是如何填充三角形中的像素。因为在三角形中我们已经将顶点的y坐标值排序,因此我们可以从最低的y坐标值进行遍历,分别计算两边的x值,代码如下:

void drawTriangle(Vec2i v0, Vec2i v1, Vec2i v2, TGAImage& image, TGAColor color)
{
    //对输入的顶点y坐标进行排序,按照v0->v1升序排列
    if (v0.y > v1.y) std::swap(v0, v1);
    if (v0.y > v2.y) std::swap(v0, v2);
    if (v1.y > v2.y) std::swap(v1, v2);

    int total_height = v2.y - v0.y;
    int semi_height = v1.y - v0.y + 1; //可能相等,因此加1,防止之后除数为0

    for (int y = v0.y; y <= v1.y; y++)
    {
	    //计算两侧的x值
        float alpha = (float)(y - v0.y) / total_height;
        float beta = (float)(y - v0.y) / semi_height;
        Vec2i A = v0 + (v2 - v0) * alpha;
        Vec2i B = v0 + (v1 - v0) * beta;
        image.set(A.x, y, red);
        image.set(B.x, y, green);
    }
}

绘制结果如下,可以看到绘制的线条会有空洞,这是之前提到的直线的陡峭和平缓造成的,但是这里我们不需要对其进行改动,因为我们的目的是填充同一y坐标轴下,坐标轴x从左到右填充颜色,因此空洞部分也会被填充,代码如下:

void drawTriangle(Vec2i v0, Vec2i v1, Vec2i v2, TGAImage& image, TGAColor color)
{
    //对输入的顶点y坐标进行排序,按照v0->v1升序排列
    if (v0.y > v1.y) std::swap(v0, v1);
    if (v0.y > v2.y) std::swap(v0, v2);
    if (v1.y > v2.y) std::swap(v1, v2);

    int total_height = v2.y - v0.y;
    int semi_height = v1.y - v0.y + 1; //可能相等,因此加1,防止之后除数为0

    for (int y = v0.y; y <= v1.y; y++)
    {
        float alpha = (float)(y - v0.y) / total_height;
        float beta = (float)(y - v0.y) / semi_height;
        Vec2i A = v0 + (v2 - v0) * alpha;
        Vec2i B = v0 + (v1 - v0) * beta;
        if (A.x > B.x) std::swap(A, B);      
        for (int i = A.x; i <= B.x; i++)
        {
            image.set(i, y, color);
        }
        image.set(A.x, y, red);
        image.set(B.x, y, green);
    }

}

在这里插入图片描述

我们完成了下半部分的三角形绘制,同样道理,我们可以使用类似方式计算上半部分的三角形。最终代码如下:

void drawTriangle(Vec2i v0, Vec2i v1, Vec2i v2, TGAImage& image, TGAColor color)
{
    //对输入的顶点y坐标进行排序,按照v0->v1升序排列
    if (v0.y > v1.y) std::swap(v0, v1);
    if (v0.y > v2.y) std::swap(v0, v2);
    if (v1.y > v2.y) std::swap(v1, v2);

    //三角形下半部分
    int total_height = v2.y - v0.y;
    int semi_height = v1.y - v0.y + 1; //可能相等,因此加1,防止之后除数为0
    for (int y = v0.y; y <= v1.y; y++)
    {
        float alpha = (float)(y - v0.y) / total_height;
        float beta = (float)(y - v0.y) / semi_height;
        Vec2i A = v0 + (v2 - v0) * alpha;
        Vec2i B = v0 + (v1 - v0) * beta;
        if (A.x > B.x) std::swap(A, B);
        
        for (int i = A.x; i <= B.x; i++)
        {
            image.set(i, y, color);
        }
    }
    
    //三角形上半部分
    semi_height = v2.y - v1.y + 1; //可能相等,因此加1,防止之后除数为0
    for (int y = v1.y; y <= v2.y; y++)
    {
        float alpha = (float)(y - v0.y) / total_height;
        float beta = (float)(y - v1.y) / semi_height;
        Vec2i A = v0 + (v2 - v0) * alpha;
        Vec2i B = v1 + (v2 - v1) * beta;
        if (A.x > B.x) std::swap(A, B);

        for (int i = A.x; i <= B.x; i++)
        {
            image.set(i, y, color);
        }
    }
}

在这里插入图片描述

上述代码有一部分重复了两边,因此我们可以对其优化,合成一个代码块。

void drawTriangle(Vec2i v0, Vec2i v1, Vec2i v2, TGAImage& image, TGAColor color)
{
    //对输入的顶点y坐标进行排序,按照v0->v1升序排列
    if (v0.y > v1.y) std::swap(v0, v1);
    if (v0.y > v2.y) std::swap(v0, v2);
    if (v1.y > v2.y) std::swap(v1, v2);

    //三角形下半部分
    int total_height = v2.y - v0.y;
	
    //这里的遍历是从v0到v2
    for (int y = v0.y; y <= v2.y; y++)
    {
        //判断是上半三角形还是下半三角形
        bool half = y >= v1.y ? true : false;

        int semi_height = half ? v2.y - v1.y + 1 : v1.y - v0.y + 1;//可能相等,因此加1,防止之后除数为0
        float alpha = (float)(y - v0.y) / total_height;
        float beta = half? (float)(y - v1.y) / semi_height : (float)(y - v0.y) / semi_height;

        Vec2i A = v0 + (v2 - v0) * alpha;
        Vec2i B = half? v1 + (v2 - v1) * beta: v0 + (v1 - v0) * beta;
        if (A.x > B.x) std::swap(A, B);
        
        for (int i = A.x; i <= B.x; i++)
        {
            image.set(i, y, color);
        }
    }
}

###行扫描

行扫描的源代码虽然并不复杂,但有点混乱。此外,它实际上是一种为单线程CPU编程设计的老派方法。下面为伪代码:

triangle(vec2 points[3]) { 
    vec2 bbox[2] = find_bounding_box(points); 
    for (each pixel in the bounding box) { 
        if (inside(points, pixel)) { 
            put_pixel(pixel); 
        } 
    } 
}

我们之前的实现的方法是首先确定三角形的边界,然后在边界中确定各个像素的颜色值,但是上面的伪代码中,我们不需要那么繁琐的过程,首先我们根据三角形确定一个可以将其包围的正方形,然后就是对正方形中的像素进行遍历,确定其是否在这个三角形内部,如果在三角形内部,则对这个像素点进行上色。

重心坐标

首先我们需要知道什么是重心坐标,给定一个二维的三角形 A B C ABC ABC和点 P P P,用一个二维的笛卡尔坐标系表示,我们的目标是找到点 P P P相对于三角形 A B C ABC ABC的重心坐标,这意味着我们需要寻找三个数字 ( 1 − u − v , u , v ) (1-u-v, u,v) (1uv,u,v),这样我们就可以如下找到 P P P:
P = ( 1 − u − v ) A + u B + v C P = A − u A − v A + u B + v C ( A − P ) + u ( B − A ) + v ( C − A ) = 0 P=(1- u - v)A + uB + vC \\ P = A - uA - vA + uB + vC \\ (A- P) + u(B-A) + v(C-A) = 0 P=(1uv)A+uB+vCP=AuAvA+uB+vC(AP)+u(BA)+v(CA)=0
这里需要明确重心和重心坐标是不一样的,重心坐标是一系列权重的组合,而重心则是其中的一个组合。上面的式子我们可以理解为使用三个权重 ( 1 − u − v , u , v ) (1-u-v,u,v) (1uv,u,v)在三个顶点ABC上。点P的坐标正好等于这个重心坐标。我们也可以换一种方式表达,将上面的式子展开再组合,我们就可以知道点P的坐标 ( u , v ) (u,v) (u,v)是在 ( A , A B → , A C → ) (A, \overrightarrow{AB},\overrightarrow{AC}) (A,AB ,AC )的基础上得到:
P = A + u A B → + v A C → P=A + u\overrightarrow{AB} + v\overrightarrow{AC} P=A+uAB +vAC
上述式子可以等价于下面
P A → + u A B → + v A C → = 0 \overrightarrow{PA} + u\overrightarrow{AB} + v\overrightarrow{AC} = 0 PA +uAB +vAC =0
我们知道这个矢量方程可以分解为两个变量x,y
{ P A x → + u A B x → + v A C x → = 0 P A y → + u A B y → + v A C y → = 0 \begin{cases}\overrightarrow{PA_x} + u\overrightarrow{AB_x} + v\overrightarrow{AC_x} = 0 \\ \overrightarrow{PA_y} + u\overrightarrow{AB_y} + v\overrightarrow{AC_y} = 0 \end{cases} {PAx +uABx +vACx =0PAy +uABy +vACy =0
我们可以将上述线性方程拆分为矩阵相乘:

{ [ u v 1 ] [ A B x → A C x → P A x → ] = 0 [ u v 1 ] [ A B y → A C y → P A y → ] = 0 \begin{cases} \left[\begin{array}{l}u & v & 1\end{array}\right] \left[\begin{array}{l} \overrightarrow{AB_x} \\ \overrightarrow{AC_x} \\ \overrightarrow{PA_x}\end{array}\right]=0 \\ \\ \left[\begin{array}{l}u & v & 1\end{array}\right] \left[\begin{array}{l}\overrightarrow{AB_y} \\ \overrightarrow{AC_y}\\ \overrightarrow{PA_y}\end{array}\right] = 0 \end{cases} [uv1] ABx ACx PAx =0[uv1] ABy ACy PAy =0
上述的式子意味着我们寻找一个同时与 ( A B x , A C x , P A x ) (AB_x, AC_x,PA_x) (ABx,ACx,PAx) ( A B y , A C y , P A y ) (AB_y, AC_y,PA_y) (ABy,ACy,PAy)正交的向量 ( u , v , 1 ) (u,v,1) (u,v,1),要找到平面上两条直线的交点(这正是我们在这里所做的),计算一个叉积就足够了。我们迭代给定三角形的边界框的所有像素。对于每个像素,我们计算其重心坐标。如果它至少有一个负分量,那么像素就在三角形之外。

现在我们所需要做的就是得到最终的 [ u , v , 1 ] [u,v,1] [u,v,1],我们可以通过对上面两个分量进行叉乘得到。代码如下:

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});
            //当[u,v,1]其中有一个分量为负时,说明改点不在三角形内部
            if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
            image.set(i, j, color);
        }
    }

}

现在我们有了改进的三角形渲染算法,我们可以尝试使用随机上色,对之前我们使用的模型中的每个面进行着色,代码如下:

int main() {
	int width = 800;
	int height = 800;
	TGAImage image(width, height, TGAImage::RGB);
    
    Model* head = new Model("./obj/african_head/african_head.obj");
    for (int i = 0; i < head->nfaces(); i++)
    {
        std::vector<int> face = head->face(i);
        Vec2i screen_coords[3];
        for (int j = 0; j < 3; j++) {
            Vec3f world_coords = head->vert(face[j]);
            screen_coords[j] = Vec2i((world_coords.x + 1.) * width / 2., (world_coords.y + 1.) * height / 2.);
        }

        Vec2i* p_coords = &screen_coords[0];
        drawTriangle(p_coords, image, TGAColor{ std::uint8_t(rand() % 255), std::uint8_t(rand() % 255), std::uint8_t(rand() % 255), std::uint8_t(255) });
    }

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

在这里插入图片描述

添加光照

接下来我们可以去掉上面的随机颜色,在三角形的面上增加光照。根据我们的生活常识,当光照与多边形面正交的时候,此时多边形面的光照强度将会最大。

img

img

当光照的向量与多边形平行的时候,平面没有光照。换言之,光照强度等于光照向量与给定三角形法线的点击,而三角形的法线可以简单的由三角形两边都叉积得到。

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");
    for (int i = 0; i < head->nfaces(); i++)
    {
        std::vector<int> face = head->face(i);

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

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

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

        Vec2i* p_coords = &screen_coords[0];
        //背面剔除
        if (intensity > 0)
            drawTriangle(p_coords, image, TGAColor{ std::uint8_t(intensity * 255), std::uint8_t(intensity * 255), std::uint8_t(intensity * 255), std::uint8_t(255) });
    }

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

光照度点积可以是负的。这意味着光线来自多边形的后面。如果场景建模良好,我们可以简单地丢弃这个三角形。这使我们能够快速移除一些不可见的三角形。这被称为背面剔除。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值