【TinyRenderer】GitHub项目——迷你渲染器(1)

目录

Lesson 0: getting started

Lesson 1: Bresenham’s Line Drawing Algorith

目前闫老师的GAMES101课程已经学完一半了,看到评论区说GitHub上有一个微渲染器的项目,可以仅仅使用几百行实现一个图形管线,感觉挺有意思的,GitHub上作者也给出了详细的教学(英文版),于是打算自己着手实现以下,正好可以全面的复习一下至今的所学内容。

GitHub项目源地址:https://github.com/ssloy/tinyrenderer

Youtube教学视频:Tiny Renderer

Lesson 0: getting started

众所周知,程序员的1总是从0开始的(bushi),这一课的目的是准备环境,可以理解为图形学中的Hello World。本文章使用的环境是Visual Studio 2019,选择创建新项目——控制台应用。

只需要添加两个文件tgaimage.h和tgaimage.cpp(用于读取和操作TGA文件)即可,不需要其他库的支持。这两个文件可以在作者的GitHub项目中找到,复制粘贴的一份到自己新建的项目文件夹中,然后在项目中添加就可以了。

在main.cpp函数中添加一下测试代码:

#include "tgaimage.h"

const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);

int main(int argc, char** argv) {
    TGAImage image(100, 100, TGAImage::RGB);
    image.set(52, 41, red);
    image.flip_vertically(); // 将坐标系原点设置在左下角
    image.write_tga_file("output.tga");
    return 0;
}

测试代码定义了两个颜色white和red,在main函数中创建了一个大小为100×100的画布,坐标系原点设置在画布的左下角,并在(52,41)的位置点了一个红色的像素点,得到输出结果output.tga文件,打开应该是这个样子的。

 OK!Lesson 0到此结束,非常的简单方便。

Lesson 1: Bresenham’s Line Drawing Algorithm

第1课正式开始编写我们的微渲染器,想要在屏幕上绘制各种图像,首先要从最简单的画线开始。

First Attempt

如果想把两点(x0, y0)和 (x1, y1)之间用线段连接起来, 最简单的实现方式是构造一个如下的画线函数,并在main函数中调用。

void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
    for (float t = 0; t <= 1; t += 0.01) {
        int x = x0 + (x1 - x0) * t;
        int y = y0 + (y1 - y0) * t;
        image.set(x, y, color);
    }
}
line(13, 20, 80, 40, image, white);

该函数可以理解为在(x0, y0)和 (x1, y1)之间进行线性插值,虽然使用测试代码输出的结果看似不错,但是由于整个循环执行的次数是固定的(1 / 0.01 = 100次),所以当我们需要绘制不同长度的线段时,需要每次都调整t的大小,这显然是非常不合理的。

Second Attempt

第一次尝试之所以是错误的,其原因在于绘制的像素个数是固定的,我们预期的结果是根据两点坐标,自动地计算出需要绘制多少个连续像素点,于是有了下面的代码:

void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
    for (int x = x0; x <= x1; x++) {
        float t = (x - x0) / (float)(x1 - x0);
        int y = y0 * (1 - t) + y1 * t;
        image.set(x, y, color);
    }
}

先来整理一下这几行代码的原理:我们的目标是连续的绘制从(x0, y0)到 (x1, y1)的若干个像素点,那么可以x坐标每增加1就绘制一个点,所以循环的总次数(像素点个数)为x1 - x0,既然x坐标已经,那么接下来还需要求出y坐标的值,根据如下比例关系:

\frac{x-x_{0}}{x_{1}-x_{0}} = \frac{y-y_{0}}{y_{1}-y_{0}}\rightarrow y=(1-t)y_{0}+ty_{1}

即可得到每个x对应的y值,我们怀着激动的心情绘制一下三条线段:

line(13, 20, 80, 40, image, white); 
line(20, 13, 40, 80, image, red); 
line(80, 40, 13, 20, image, red);

得到的结果却是这个样子的

第一条白色线段(12, 20)到(80, 40)是正常的;

第二条红色线段(20, 13)到(40, 80)很明显出现了不连续的现象;

第三条红色线段(80, 40)到(12, 20)完全不存在,这是一个对称性测试,绘制一条线段不应该依赖于点的顺序,(a, b)和(b, a)应该得到同样的结果。

Third Attempt

先解决对称性的问题,可以通过交换两个点的位置使得x0总是小于x1。而在线段中出现不连续现象的原理,是因为线段最小外接矩形的高 > 宽,或者可以理解为线段上升地太快了(斜率太大),解决方案是,将线段进行转置,绘制的时候再逆转置。

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);
    }
    for (int x = x0; x <= x1; x++) {
        float t = (x - x0) / (float)(x1 - x0);
        int y = y0 * (1. - t) + y1 * t;
        if (steep) {
            image.set(y, x, color); // 如果x,y转置,则进行逆转置 
        }
        else {
            image.set(x, y, color);
        }
    }
}

得到的结果正如我们所需要的:

 但是当我们绘制对角线的时候,还是会存在一点点问题,线段看起来不是那么直,不是很稳定的样子。

Forth Attemp

在这次试验中,我们增加一个误差变量error,每当error大于一个像素是,我们将增加(或减少)1个单位像素,同时将error - 1。

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;
    float derror = std::abs(dy / float(dx));  // 斜率,即error每次随x增(减)的值
    float error = 0;
    int y = y0;
    for (int x = x0; x <= x1; x++) {
        if (steep) {
            image.set(y, x, color);
        }
        else {
            image.set(x, y, color);
        }
        error += derror;
        if (error > 0.5) {
            y += (y1 > y0 ? 1 : -1);
            error -= 1.;
        }
    }
}

这次我们得到了看起来不错的对角线

Fifth Attempt

最后对目前为止的算法进行优化,我们想完全去掉函数中的浮点数部分,这样可以使计算更加快速 ,先把优化过的代码贴出来。通过对比可以明显看出来,和ForthAttempt的主要差别是修改了derror和error两个变量,很有意思的事情是,在Youtube的视频里,作者本人也对于这种优化方法可以实现感到疑惑,所以并没有对该部分代码进行过多的讲解。

不过我的理解是,优化后的代码将原来以一个像素单位的研究尺度进行放大2*dx倍,这样derror从dy/dx扩大为2dy,error的判断标准从0.5扩大为dx,增减大小由1变为2dx。

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;
        }
    }
}

 以上就是Bresenham's画线算法的最终版本

Wireframe Rendering

如何绘制两点之间的线段问题解决了,那么就可以正式开始渲染线框架了。首先需要在项目文件添加model.h、model.cpp、geometry.h以及obj文件。

先用记事本打开obj文件看看里面是什么样子的:

v -0.000581696 -0.734665 -0.623267
v 0.000283538 -1 0.286843
v -0.117277 -0.973564 0.306907
...
# 1258 vertices

vt  0.532 0.923 0.000
vt  0.535 0.917 0.000
vt  0.542 0.923 0.000
...
# 1339 texture vertices

vn  0.001 0.482 -0.876
vn  -0.001 0.661 0.751
vn  0.136 0.595 0.792
...
# 1258 vertex normals

g head
s 1
f 24/1/24 25/2/25 26/3/26
f 24/1/24 26/3/26 23/4/23
f 28/5/28 29/6/29 30/7/30
...
# 2492 faces

v代表三角形的每个顶点(x, y, z),vt和vn分别为顶点的切线和法线;

f有三组数据组成,每一组的各个元素,分别代表三角面的顶点、切线和法线对应顶点序号。

例如f 24/1/24 25/2/25 26/3/26,说明该三角形的三个顶点分别为(24, 25, 26),对应的切线为(1, 2, 3),对应的法线为(24, 25, 26)。

model文件为一个简单的解析器,用于解析obj文件并获取所需信息,最后在主函数中通过循环,就可以得到一个渲染后的线框架

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值