EasyShader——线框与三角形
前言
- 本节分享需要一个obj格式的模型文件,我的库里提供了这个文件,当然,如果你具备建模能力也可以自己建一个模型使用,重要的是思路,不是素材!当然素材一样会提供,并且这节分享需要更多的资源我们一一来看。
- model.h
- 这个头文件是用来导入模型的,它能够导入后缀为obj的模型类型。
- Vector
- 这是c++为我们提供的库文件,它叫做容器,内部维护着一个动态数组,我们通过它来讲模型数据导入并输出成为像素点。
2.1 重构mainc.cpp
2.1.1 头文件项更改
- 首先我们必须加入以上提到的头文件,依次获取其中的变量与方法。
1 |
2.1.2 全局变量与main函数修改
- 我们需要创建一个Model * 类型的变量,它本身是个Model类型的指针,对应着一片Model长度的内存地址,我们用其来存储我们读取到的模型。
1 | Model *model = NULL; |
- 我们现将其设置为空,请注意,在此提醒一个编程小技巧,我们能够申请到的空间叫做堆,我们需要谨慎小心地操作内存空间,所以一定要对每一个声明的指针变量对应一个确定的内存区域,NULL区域在不同的系统中对应空间不一样,但是它是安全的,所以我们一开始将其赋于我们的模型变量,让它安全的申请下来。
- 接下来是main函数部分
- 首先我们需要让main函数具有两个参数,你可能觉得这不符合你的习惯,确实,我们一般不在main函数中声明参数,但main函数实际上是程序运行的入口函数,它是被系统调用的函数之一,因此我们一样完全可以为其添加参数
- 这两个参数是为了防止读取模型失败导致内存泄漏而存在的,如果模型正常加载则写入我们存放在对应路径下的文件(注意是相对路径),否则交给model内的方法处理。
1 | int main(int argc, char** argv) { |
- 接下来,我们需要对.obj文件进行读取,如果你打开.obj文件,其实可以发现,它就是一大堆的点坐标,我们需要的就是依次读入点坐标所形成的面,并调用我们之前的line算法,将其连接并画在画布上。
1 | for (int i=0; i<model->nfaces(); i++) { |
- 我们遍历模型中所有的面,并将其全部装到一个int类型的数组中,而后我们以3为一个单位,去遍历这个数组,并将其中的点取出作为v0和v1,而后我们将这些点缩放到屏幕空间(希望你还记得这个概念),最后调用绘制算法,将图像用白色绘制到画布上!
- 不过在这里你得到的可能是反着的,这和坐标系不同有关(希望你同样记得这个概念),接下来我们利用TGAIMAGE内置的方法,将这个图像统统翻转!然后绘制就好了,但最后别忘记释放你申请的内存!这跟申请一样同样需要你小心翼翼!
1 | image.flip_vertically(); |
2.1.3 物体线框渲染
- 现在,你就可以得到你的大苹果了!
2.1.2 线框大苹果
- 很有成就感是不是?但这远远没有结束,我们将做的更酷!最后我们完成时,这将是个有材质的苹果。
2.2 三角形
2.2.1 三角形线框绘制
- 首先,三角形是由基本的三个点组成的,因此我们如果要定义一个绘制三角形点的函数,这并不困难,我们只需要让其包含三个最基本的点坐标,并用合适的颜色绘制它就好了,这和我们绘制苹果线框的逻辑是一样的,所以在这里我们可以利用line方法写出一个初步的方案
1 | void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) { |
- 让我们测试一下这个函数,我们绘制三个三角形
1 | Vec2i t0[3] = {Vec2i(0, 70), Vec2i(10, 160), Vec2i(10, 80)}; |
- 最后你应该会得到这张图像
2.2.1 简单的三角形线框
2.2.2 三角形填充原理
- 实际上,这个问题的解决方法有很多,我们可以尝试很多不同的方法,在此我们先根据最简单的思路,那就是扫描线绘制,我们可以一条一条的填充我们的三角形,这条线的y值是固定的,x值由左端到右端变化,我们只需要依次调用我们之前写好了的DrawLine方法,绘制就可以了,伪代码如下
1 | For each horizontal line y between the triangle‘s top and Bottom |
- 接着,我们可以将y值进行排序,选出三个点之中的最小值和最大值,并将其分别命名为y0,y2,所以y的取值就是在y0——y2区间内。
- 随后我们关注我们的x_left与x_right,我们希望他们包含整个三角形的全部取值范围,因此我们需要关注不同形态的三角形,并计算其边的x值。
- 对于三角形我们可以通过y值分出长边和短边,在这里我们统一定义P0——P2为高边。x_right的值要么来自高边,要么来自短边
2.2.2 分离三角形的高边与短边
- 因此我们可以使用插值计算这些边中y值对应的x值,其实就是之前我们绘制直线方法的变体。我们将这些数据记录在三个数组之中,并且合并x01 和 x12 数组为x012 也就是整个三角形的x变量数组。
- 随后我们就可以判断谁是x_left中的数谁是x_right中的数了,只需要选择任何一条水平线,比较其在x02与x012中的值,如果x02 小于x012,那么就说明x02中的值为x_left否则为x_right。至此我们完成了绘制方法,接着调用DrawLine方法绘制即可。
- 接下来我们来看代码实现
2.3 代码实践
2.3.1 三角形长短边分类
- 首先来按我们的思路,给三角形长短边分分类吧,这在代码实现中很简单,我们只需要判断谁大谁小,然后给他们排序就好了。
1 | void triangleLine(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color) |
2.3.2 上下区域分离
- 现在我们已经可以区分长边和短边了,下一步我们就将对三角形进行分解,因为短边始终会有两个,因此我们会将这两个边按其交点处分解,分别绘制!
1 | void ApartOfTriangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color) |
- 现在我们已经可以描绘三角形下半部分的线框了,但这样的线性插值难免会有问题,因为步长的缘故所以会出现断线!
2.3.2 断线问题
- 这个问题其实我们可以忽略,因为最后绘制时我们用对应的水平线连接这些点,间隙就会消失。
2.3.2 填充三角形并绘制上半部分
- 我们可以直接简单的使用一个for循环去绘制这些连接两段的直线,为了避免出现问题,我们在设置直线时先判断大小,若大小相反则将其翻转并绘制
1 | void FilledTriangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage image, TGAColor color) |
- 恭喜你,你现在已经可以绘制一个实心三角形了,但是我们可以做的更好
- 首先被考虑到的就是,我们可以对代码层面进行优化,因为现在存在四个for循环,总共的时间复杂度就是2n^2,我们可以将其缩减。合并为一个for循环。
1 | void FilledTriangleLv2(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage image, TGAColor color) |
- 大功告成了!。。。吗?不知道你是否还记得。在光栅化篇我们提到过的,包围盒以及后续提到的重心坐标的概念,我们如果按现在这种扫描线的方法设置,确实很简单,但效率很低,我们希望优化我们的算法,跟上时代,接下来我们将利用包围盒以及重心坐标,重新绘制三角形!
参考资料
- 计算机图形学入门——3D渲染指南
- https://github.com/ssloy/tinyrenderer
- 我的项目地址:
- https://github.com/Pleasant233/EasyRender
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Pleasant233!