• 简易的光栅化渲染器

    本文是一个完整的图形学入门实践课程,目前还在更新中,GitHub已开源。理论上本文项目需要20-30个小时完成。不知道为啥我的网站统计字数也有问题。

    主要内容是完全手撸一个光栅化渲染器。本文会从头复习图形学以及C++的相关知识,包括从零构造向量模版库、光栅化原理解释、图形学相关基础算法解释等等内容。

    另外原作者的的透视矩阵部分是经过一定程度的简化的,与虎书等正统做法不同。我会先按照原文ssloy老师的思想表达关键内容,最后按照我的想法完善本文。并且,原项目中的数学向量矩阵库写得不是很好,我专门开了一章一步步重构这个库。

    原项目链接:https://github.com/ssloy/tinyrenderer

    本项目链接:https://github.com/Remyuu/Tiny-Renderer

    简易的光栅化渲染器0 简单的开始1.1 画线第一关:实现画线第二关:发现BUG第三关:解决BUG第四关:优化前言第五关:Bresenham's 优化第六关:注意流水线预测第七关:浮点数整型化1.2 三维画线第一关:加载.obj第二关:绘制第三关:优化2.1 三角形光栅化第一关:线框三角形第二关:请你自己画实心的三角形第三关:扫描线算法第四关:包围盒逐点扫描第五关:重心坐标2.2 平面着色Flat shading render第一关:回顾第二关:绘制随机的颜色第三关:根据光线传播绘制颜色3.1 表面剔除第一关:画家算法(Painters' Algorithm)第二关:了解z-buffer第三关:创建Z-Buffer第四关:整理当前代码3.2 上贴图第一关:思路第二关:加载纹理文件第三关:获取纹理坐标第四关:通过纹理坐标uv获取对应颜色第五关:在光栅化三角形函数中增加贴贴图的功能第六关:为模板函数添加更多重载符号操作4.1 透视视角第一关:线性变换第二关:齐次坐标 Homogeneous coordinates第三关:三维世界第四关:具体代码实现4.2 项目代码分析第一关:model类第二关:geometry第三关:main5.1 移动摄像机第一关:定义摄像机第二关:相机代码6.1 优化/重写代码6.2 重写模版向量类第一关:需求分析第二关:实现Vec2模版以及四个算数符第三关:实现Vec3模版以及四个算数符第四关:用模版构建不同大小的向量第五关:进一步完善向量功能第六关:构建矩阵第七关:继续完善矩阵库6.3 整合光栅化代码特别节目1之:main代码之旅特别节目2之:细说GouraudShader特别节目3之:开始绘画-片元着色器🌈 彩虹着色器📺 模拟老电视效果🔥 火焰效果🌌 星空效果6.4 升级Shader-支持UV纹理🎬 Shader类的角色列表🎭 vertex函数:多面手🌈 fragment函数:艺术家🎨 那么,这个Shader类都能做什么?6.5 学习法线贴图第一关:纹理第二关:全局坐标系与Darboux坐标系第三关:经常见到的Uniform第四关:光照计算6.6 实现Phong模型7.1 阴影第一关:目前的问题第二关:第一趟渲染-从光源出发第三关:第二趟渲染-从主相机出发8.1 环境光遮蔽 - 模拟全局光照效果特别节目:知识脉络第一关:啥是AO?如何结合Phong使用?第二关:做梦第三关:屏幕空间环境遮挡 (SSAO)附录1. c++模版类 - 从入门到入土第一关:为什么需要模版类?第二关:「函数模版」第三关:「类模版」第四关:「多模板参数」与「非类型参数」第五关:「模板特化」第六关:「类型推断」1. auto & decltype2. 模板中的基本类型推断3. 自动构造模版类型4. 尾返回类型第七关:「变量模板」第八关:「模板类型别名」第九关:模板的SFINAE原则第十关:模板与友元第十一关:折叠表达式第十二关:模板概念(Concepts) - C++20第十三关: std::enable_if 和 SFINAE第十四关:类模板偏特化第十五关:constexpr 和模板第十六关:模板中的嵌套类型第十七关:模板参数包与展开第十八关:Lambda 表达式与模板第十九关:模板递归第二十关:带有模板的继承1. 模版基类2. 模版子类3. 在模板类中继承模板基类第二十一关:std::type_trait的工具集第二十二关:模板与动态多态性备注/声明

    0 简单的开始

    五星上将曾经说过,懂的越少,懂的越多。我接下来将提供一个tgaimage的模块,你说要不要仔细研究研究?我的评价是不需要,如学。毕竟懂的越多,懂的越少。

    在这里提供一个最基础的框架🔗,他只包含了tgaimage模块。该模块主要用于生成.TGA文件。以下是一个最基本的框架代码:

    上面代码会创建一个100*100的image图像,并且以tga的格式保存在硬盘中。我们在TODO中添加代码:

    代码作用是在(1, 1)的位置将像素设置为红色。output.tga的图像大概如下所示:

    0.0

    1.1 画线

    这一章节的目标是画线。具体而言是制作一个函数,传入两个点,在屏幕上绘制线段。

    第一关:实现画线

    给定空间中的两个点,在两点(x0, y0)(x1, y1)之间绘制线段。

    最简单的代码如下:

    image-20230830171306158

    第二关:发现BUG

    上面代码中的.01其实是错误的。不同的分辨率对应的绘制步长肯定不一样,太大的步长会导致:

    image-20230830171714587

    所以我们的逻辑应该是:需要画多少像素点就循环Draw多少次。最简单的想法可能是绘制x1-x0个像素或者是y1-y0个像素:

    上面代码是最简单的插值计算。但是这个算法是错误的。画三条线:

    image-20230830172534739

    白色线看起来非常好,红色线看起来断断续续的,蓝色线直接看不见了。于是总结出以下两个问题:

    1. 理论上说白色线和蓝色线应该是同一条线,只是起点与终点不同

    2. 太“陡峭”的线效果不对

    接下来就解决这个两个问题。

    此处“陡峭”的意思是(y1-y0)>(x1-x0)

    下文“平缓”的意思是(y1-y0)<(x1-x0)

    第三关:解决BUG

    为了解决起点终点顺序不同导致的问题,只需要在算法开始时判断两点x分量的大小:

    为了画出没有空隙的“陡峭”线,只需要将“陡峭”的线变成“平缓”的线。最终的代码:

    image-20230830174932047

    如果你想测试你自己的代码是否正确,可以尝试绘制出以下的线段:

    image-20230831145010708

    第四关:优化前言

    目前为止,代码运行得非常顺利,并且具备良好的可读性与精简度。但是,画线作为渲染器最基础的操作,我们需要确保其足够高效。

    性能优化是一个非常复杂且系统的问题。在优化之前需要明确优化的平台和硬件。在GPU上优化和CPU上优化是完全不同的。我的CPU是Apple Silicon M1 pro,我尝试绘制了9,000,000条线段。

    发现在line()函数内,image.set();函数占用时间比率是38.25%,构建TGAColor对象是19.75%,14%左右的时间花在内存拷贝上,剩下的25%左右的时间花费则是我们需要优化的部分。下面的内容我将以运行时间作为测试指标。

    第五关:Bresenham's 优化

    我们注意到,for循环中的除法操作是不变的,因此我们可以将除法放到for循环外面。并且通过斜率估计每向前走一步,另一个轴的增量error。dError是一个误差积累,一旦误差积累大于半个像素(0.5),就对像素进行一次修正。

    没有优化用时:2.98s

    第一次优化用时:2.96s

    第六关:注意流水线预测

    在很多教程当中,为了方便修改,会用一些trick将“陡峭”的线和“平缓”的线的for循环代码整合到一起。即先将“陡峭”线两点的xy互换,最后再image.set()的时候再换回来。

    没有优化用时:2.98s

    第一次优化用时:2.96s

    合并分支用时:3.22s

    惊奇地发现,竟然有很大的性能下降!背后的原因之一写在了这一小节的标题中。这是一种刚刚我们的操作增加了控制冒险(Control Hazard)。合并分支后的代码每一次for循环都有一个分支,可能导致流水线冒险。这是现代处理器由于预测错误的分支而导致的性能下降。而第一段代码中for循环没有分支,分支预测可能会更准确。

    简而言之,减少for循环中的分支对性能的提升帮助非常大!

    值得一提的是,如果在Tiny-Renderer中使用本文的操作,速度将会进一步提升。这在Issues中也有相应讨论:链接🔗

    第七关:浮点数整型化

    为什么我们必须用浮点数呢?在循环中我们只在与0.5做比较的时候用到了。因此我们完全可以将error乘个2再乘个dx(或dy),将其完全转化为int。

    没有优化用时:2.98s

    第一次优化用时:2.96s

    合并分支用时:3.22s

    第二次优化用时:2.96s

    优化程度也较为有限了,原因是在浮点数化整的过程中增加了计算的次数,与浮点数的计算压力相抵消了。

    1.2 三维画线

    在前面的内容中,我们完成了Line()函数的编写。具体内容是给定屏幕坐标上的两个点就可以在屏幕中绘制线段。

    第一关:加载.obj

    首先,我们创建model类作为物体对象。我们在model加载的.obj文件里可能会有如下内容:

    v表示3D坐标,后面通常是三个浮点数,分别对应空间中的x, y, z。上面例子代表一个顶点,其坐标为 (1.0, 2.0, 3.0)

    当定义一个面(f)时,你引用的是先前定义的顶点(v)的索引。

    上面两行都表示一个面,

    在这里我提供一个简单的 .obj 文件解析器 model.cpp 。你可以在此处找到当前项目链接🔗。以下是你可能用到的model类的信息:

    本项目使用的.obj文件的所有顶点数据已做归一化,也就是说v后面的三个数字都是在[-1, 1]之间。

    第二关:绘制

    在这里我们仅仅考虑三维顶点中的(x, y),不考虑深度值。最终在main.cpp中通过model解析出来的顶点坐标绘制出所有线框即可。

    这段代码对所有的面进行迭代,将每个面的三条边都进行绘制。

    image-20230831145239967

    第三关:优化

    将不必要的计算设置为const,避免重复分配释放内存。

    2.1 三角形光栅化

    接下来,绘制完整的三角形,不光是一个个三角形线框,更是要一个实心的三角形!为什么是三角形而不是其他形状比如四边形?因为三角形可以任意组合成为所有其他的形状。基本上,在OpenGL中绝大多数都是三角形,因此我们的渲染器暂时无需考虑其他的东西了。

    当绘制完一个实心的三角形后,完整渲染一个模型也就不算难事了。

    在Games101的作业中,我们使用了AABB包围盒与判断点是否在三角形内的方法对三角形光栅化。你完全可以用自己的算法绘制三角形,在本文中,我们使用割半法处理。

    第一关:线框三角形

    利用上一章节完成的line()函数,进一步将其包装成绘制三角形线框的triangleLine()函数。

    image-20230831145749059

    第二关:请你自己画实心的三角形

    这一部分最好由你自己花费大约一个小时完成。一个好的三角形光栅化算法应该是简洁且高效的。你目前的项目大概是这样的:链接🔗

    【此处省略一小时】

    image-20230831213131128

    第三关:扫描线算法

    当你完成了你的算法之后,不妨来看看其他人是怎么做的。为了光栅化一个实心三角形,一种非常常见的方法是使用扫描线算法:

    1. v(或 y)坐标对三角形的三个顶点进行排序,使得 v0 是最低的,v2 是最高的。

    2. 对于三角形的每一行(从 v0.vv2.v),确定该行与三角形的两边的交点,并绘制一条从左交点到右交点的线。

    第四关:包围盒逐点扫描

    介绍另一个非常有名的方法,包围盒扫描方法。将需要光栅化的三角形框上一个矩形的包围盒子内,在这个包围盒子内逐个像素判断该像素是否在三角形内。如果在三角形内,则绘制对应的像素;如果在三角形外,则略过。伪代码如下:

    想要实现这个方法,主要需要解决两个问题:找到包围盒、判断某个像素点是否在三角形内。

    第一个问题很好解决,找到三角形的三个点中最小和最大的两个分量两两组合。

    第二个问题似乎有些棘手。我们需要学习什么是重心坐标 (barycentric coordinates )。

    第五关:重心坐标

    利用重心坐标,可以判断给定某个点与三角形之间的位置关系。

    给定一个三角形ABC和任意一个点P (x,y) ,这个点的坐标都可以用点ABC线性表示。不理解也无所谓,简单理解就是一个点P和三角形三点的关系可以用三个数字来表示,像下面公式这样:

    P=(1uv)A+uB+vC

    我们把上面的式子解开,得到关于 AB,ACAP的关系:

    P=A+uAB+vAC

    然后将点P挪到同一边,得到下面的式子:

    uAB+vAC+PA=0

    然后将上面的向量分为x分量与y分量,写成两个等式。接下来用矩阵表示他们:

    {[uv1][ABxACxPAx]=0[uv1][AByACyPAy]=0

    两个向量点积是0,说明两个向量垂直。右边这俩向量都与 [uv1] ,说明他们的叉积就是k[uv1] ,因此轻轻松松解出uv。

    梳理一下,当务之急是判断给定的一个点与一个三角形的关系。直接给出结论,如果点在三角形内部,则这三个系数都属于(0,1)之间。直接给出光栅化一个三角形的代码:

    barycentric()函数可能比较难理解,可以暂时抛弃研究其数学原理。并且上面这段代码是经过优化的,如果希望了解其原理可以看我这一篇文章:链接🔗

    image-20230904005910991

    你可以在下面的链接中找到当前项目的代码:链接🔗

    2.2 平面着色Flat shading render

    在「1.2 三维画线」中绘制了模型的线框,即空三角形模型。在「2.1 三角形光栅化」中,介绍了两种方法绘制一个“实心”的三角形。现在,我们将使用“平面着色”来渲染小人模型,其中平面着色使用随机的RGB数值。

    第一关:回顾

    首先将加载模型的相关代码准备好:

    第二关:绘制随机的颜色

    下面是遍历获得模型的每一个需要绘制的三角形的代码:

    当我们获得了所有的面,在每一趟遍历中,将face的三个点取出来并转换到屏幕坐标上,最后传给三角形光栅化函数:

    image-20230904134928856

    第三关:根据光线传播绘制颜色

    刚才的随机颜色远远满足不了我们,现在我们根据光线与三角形的法线方向绘制不同的灰度。什么意思呢?看下面这张图,当物体表面的法线方向与光线方向垂直,物体接受到了最多的光;随着法线与光线方向的夹角越来越大,收到光的照射也会越来越少。当法线与光线方向垂直的时候,表面就接收不到光线了。

    image-20230904135449781

    将这个特性添加到光栅化渲染器中。

    上面代码需要注意的点:

    intensity小于等于0的意思是这个面(三角形)背对着光线,摄像机肯定看不到,不需要绘制。

    image-20230904141708453

    注意到嘴巴的地方有些问题,本应在嘴唇后面的嘴巴内部区域(像口腔这样的空腔)却被画在嘴唇的上方或前面。这表明我们对不可见三角形的处理方式不够精确或不够规范。“dirty clipping”方法只适用于凸形状。对于凹形状或其他复杂的形状,该方法可能会导致错误。在下一章节中我们使用 z-buffer 解决这个瑕疵(渲染错误)。

    这里给出当前步骤的代码链接🔗

    3.1 表面剔除

    上一章的末尾我们发现嘴巴部分的渲染出现了错误。本章先介绍画家算法(Painters' Algorithm),随后引出 Z-Buffer ,插值计算出需渲染的像素的深度值。

    第一关:画家算法(Painters' Algorithm)

    这个算法很直接,将物体按其到观察者的距离排序,然后从远到近的顺序绘制,这样近处的物体自然会覆盖掉远处的物体。

    但是仔细想就会发现一个问题,当物体相互阻挡时算法就会出错。也就是说,画家算法无法处理相互重叠的多边形。

    image-20230904144410471

    第二关:了解z-buffer

    如果画家算法行不通,应该怎么解决物体相互重叠的问题呢?我们初始化一张表,长宽与屏幕像素匹配,且每个像素大小初始化为无限远。每一个像素存储一个深度值。当要渲染一个三角形的一个像素时,先比较当前欲渲染的像素位置与表中对应的深度值,如果当前欲渲染的像素深度比较浅,说明欲渲染的像素更靠近屏幕,因此渲染。

    而这张表,我们称之为:Z-Buffer。

    第三关:创建Z-Buffer

    理论上说创建的这个 Z-Buffer 是一个二维的数组,例如:

    但是,我认为这太丑陋了,不符合我的审美。我的做法是将二维数组打包变成一个一维的数组:

    最基本的数据结构,取用的时候只需要:

    初始化zBuffer可以用一行代码解决,将其全部设置为负无穷:

    第四关:整理当前代码

    要给当前的triangleRaster()函数新增 Z-Buffer 功能。

    我们给pixel增加一个维度用于存储深度值。另外,由于深度是float类型,如果沿用之前的函数可能会出现问题,原因是之前传入的顶点都是经过取舍得到的整数且不包含深度信息。而且需要注意整数坐标下的深度值往往不等于取舍之前的深度值,这个精度的损失带来的问题是在复杂精细且深度值波动很大的位置会出现渲染错误。但是目前可以直接忽略,等到后面进行超采样、抗锯齿或者其他需要考虑像素内部细节的技术时再展开讲解。

    因此,为了后期拓展的方便,我们将之前涉及pixel的Vec2i代码换为Vec3f类型,并且每一个点都增加一个维度用于存储深度值。

    将世界坐标转化到屏幕坐标的函数打包:

    另外,对tgaimage、model和geometry做了一些修改,主要是优化了一些细节。具体项目请查看当前项目分支链接🔗

    image-20230904191612606

    3.2 上贴图

    啥是贴图呢?就是类似这种奇奇怪怪的图片。

    image-20230905174124334

    目前我们已经完成了三角形的重心坐标插值得出了三角形内某点的深度值。接下来我们还可以用插值操作计算对应的纹理坐标。

    本章基于「3.1 表面剔除」最后的项目完善,本章主要是c++ STL相关操作。

    第一关:思路

    请首先下载「3.1 表面剔除」最后的项目链接🔗

    首先从硬盘中加载纹理贴图,然后传到三角形顶点处,通过对应的纹理坐标从texture获取颜色,最后插值得到各个像素的颜色。

    另外,项目框架的代办清单:

    1. 增加model模块中对vt标签的解析

    2. 完善model模块中对f标签的解析,具体是获取纹理坐标索引

    3. 完善geometry模块的操作符,具体是实现Vec<Dom, f>与float相乘等操作

    第二关:加载纹理文件

    从硬盘中加载纹理texture,用TGAImage存储。

    第三关:获取纹理坐标

    在 model.h 中,在class Model上方创建一个Face结构体用于存储解析后obj中的f标签。f标签有三个值,这里只存储前两个。f标签的三个值分别是顶点索引/纹理索引/法线索引,等后面用到了法线坐标再拓展即可。

    然后将model的模版私有属性:

    改为:

    同时也修改 model.cpp 下获取 face 的函数:

    实际解析时的函数:

    接下来解析纹理坐标索引texcoords_。

    最后就可以通过对应的索引得到纹理坐标了。

    第四关:通过纹理坐标uv获取对应颜色

    获得了纹理坐标后就可以用texture.get(x_pos, y_pos)获取图片(贴图/纹理)的对应像素。注意最后TGAColor使用的是BGRA通道,而不是RGBA通道。

    第五关:在光栅化三角形函数中增加贴贴图的功能

    增加了四个传参,分别是三个三角形的纹理坐标与纹理。实现细节直接看代码比较直接。

    在上面的代码中,你可能会发现乘号竟然报错了,这个问题在下一关马上得到解决。最终在 main() 函数中这样调用:

    image-20230905084358296

    第六关:为模板函数添加更多重载符号操作

    在写纹理坐标的时候,我们会用到一些操作比如说 Vec2i 类型与 float 浮点数相乘和相除。将下面的代码添加到 geometry.h 的中间部分:

    这样就完全没问题了,大功告成。当然你也可以在这个链接🔗中找到完整的代码。

    4.1 透视视角

    上文的内容全部都是正交视角下的渲染,这显然算不上酷,因为我们仅仅是将z轴“拍扁”了。这一章节的目标是学习绘制透视视角。

    image-20230409155021065

    https://stackoverflow.com/questions/36573283/from-perspective-picture-to-orthographic-picture

    第一关:线性变换

    缩放可以表示为:

    scale(sx,sy)=[sx00sy].

    image-20230408154330557

    拉伸可以表示为:

    shear-x(s)=[1s01],shear-y(s)=[10s1]

    image-20230408154937046

    旋转可以表示为:

    Rθ=[cosθsinθsinθcosθ]

    image-20230408155212728

    第二关:齐次坐标 Homogeneous coordinates

    为什么要引入齐次坐标呢?因为想要表示一个二维变换的平移并不能仅仅使用一个2x2的矩阵。平移并不在这个二维矩阵的线性空间中。因此,我们拓展一个维度帮助我们表示平移。

    在计算机图形学中我们使用齐次坐标(Homogeneous Coord)。比如说一个二维的(x,y)使用平移矩阵变换到(x,y)

    (xyw)=(10tx01ty001)(xy1)=(x+txy+ty1)

    这样,我们就可以通过 tx,ty 做平移变换,简直太聪明了。

    在常规的笛卡尔坐标中,很难从数学表示上区分一个点和一个向量,因为它们都可能使用相同的形式如 vec2(x,y)。但在齐次坐标中,通过最后一个坐标值(这里的z)可以明确区分它们。当z=0时,它是一个向量;当z≠0时,它是一个点。较为数学一点的表示方法:

    k[xy1],k0

    上面公式中,无论 k 取多少,都表示同一个点。再举个例子:

    [xy1][2x2y2][514x514y514][114x114y114]

    齐次坐标是一个大大的好啊,当你进行数学操作时,结果的类型(向量或点)是明确的:

    这使得数学操作更加直观和有意义。

    一段来自屏幕外的声音🔊:齐次坐标最下面那行有啥用??这个问题非常关键。

    [abmcdnpq1]

    家喻户晓的,[abcd]可以实现缩放,[mn1] 可以实现平移。

    但是,这个 [pq] 能干嘛?

    变换矩阵不做其他线性变换,仅仅将pq随便设为一个数:

    [100010201][xy1]=[xy2x+1][x2x+1y2x+11]

    我们发现,这个变换有点奇怪。随着(x,y)越来越大,这个“缩放因子”就会越来越小。

    有没有一种可能,这个就是近大远小?

    image-20230905160328502

    没错,这就是一种透视的现象。至此,上面齐次坐标矩阵的最后一朵乌云已经攻破。 [pq] 就是用来做透视变换的。

    随着最后一朵乌云散去,必然会迎来更多的乌云。新的乌云,名字叫做三维。

    第三关:三维世界

    上文所述都是二维下的,现在进入三维的世界。三维的齐次坐标自然就是用四维的矩阵表示。

    缩放:

    S(sx,sy,sz)=(sx0000sy0000sz00001)

    平移:

    T(tx,ty,tz)=(100tx010ty001tz0001)

    绕x,z,y轴旋转:

    {Rx(α)=(10000cosαsinα00sinαcosα00001)Rz(α)=(cosαsinα00sinαcosα0000100001)Ry(α)=(cosα0sinα00100sinα0cosα00001)

    透视:

    P(r)=(10000100001000r1)

    为什么只有z方向上才有r?因为我们默认摄像机摆在z轴,物体随着z轴透视缩放的。

    现在,将一个三维坐标通过透视缩放,得到:

    [10000100001000r1][xyz1]=[xyz1+zr][x1+zry1+zrz1+zr1]

    image-20230905173407444

    上图中,横轴向左是z的正方向,纵轴向上是y的正方向。

    根据相似三角形法则,y1/By=(c-z1)/c,最后得到:

    r=1c

    因此得到透视矩阵:

    [100001000010001c1]

    大家可能发现,如果有接触过图形学的朋友们可能会对之前的学习产生怀疑。为什么这里顶点变换的透视变换矩阵和其他教材都不一样呢?比方说虎书上的透视矩阵是这样的:

    [1 aspect ×tan(fovy2)00001tan( fovy 2)0000 far + near  far  near 2× far × near  far  near 0010]

    所以我们刚才推导出来的矩阵并不是常见的透视投影矩阵,但是他确实表达了投影的思想,因此我们暂时用着。

    来自屏幕外的声音🔊:停停停,理论说了这么多,能不能搞点实践的!

    第四关:具体代码实现

    在上一关中,我们得到了不那么正规但是能用的透视矩阵,现在要做的就是将世界坐标的顶点转换到齐次坐标,然后乘上透视矩阵、视口矩阵。视口矩阵其实就是用一个简洁的矩阵把下面归一化设备坐标 NDC [-1,1]转换到了屏幕空间[0,width]。看下面这一段代码就是那个被ViewPort矩阵淘汰的家伙:

    接下来,把顶点坐标乘上我们下面两个矩阵(顺序要注意):

    一段来自屏幕外的声音🔊:等等等等,v2m和m2v是什么?viewport()具体实现方法是什么?

    v2m是将向量变成矩阵(齐次坐标),m2v反之。

    然后还需要完善geometry的模块,在geometry.h中添加如下代码:

    然后添加文件 geometry.cpp

    接下来,渲染器启动!

    image-20230906112817473

    来自甲方的声音🔊:效果非常好,下次不要做了。

    看得出来,画面出现了一点问题。但是值得注意的是,顶点的位置已经基本正确了。但是贴图出现了错误。

    借此机会,调整一下贴图加载的逻辑。我们原先在main函数粗暴加载,现在我们将物体对应的贴图当作model对象的一个属性,自动读取。在model.h中加入字段:

    构造函数就可以根据文件名字存入对应的贴图了:

    然后通过以下函数得到对应的uv坐标:

    改动部分比较多直接阅读项目吧,项目链接🔗在这里,我们在「4.2 代码分析」中详细讨论整个项目,力求搞懂每一行代码与设计思路,尤其是C++ STL细节。下面是一个最终结果:

    image-20230906183206037

    4.2 项目代码分析

    目前的代码链接🔗有较大的改动,但是技术原理是不变的。本章节可以选择性阅读,也可以直接跳到「5.1 移动摄像机」。

    项目结构:

    第一关:model类

    model.h

    Model.cpp

    第二关:geometry

    geometry.h中,分为两个部分:模版向量类,矩阵类