这是此项目的github地址:https://github.com/xingyuZHOU6/Games101-SoftRender.git
架构思路:
通过渲染管线进行架构,渲染管线的流程描述,渲染管线的目的是将一个三维场景转变为一张二维的图,并绘制在屏幕上。
渲染管线的流程:假设我们在三维场景中有一些模型,那这些模型一定具有顶点数据,顶点中保存着顶点的位置(默认为模型空间下),法线,纹理,颜色等信息,首先,我们先进入应用阶段,我们可以在这个阶段选择不同的顶点着色器和片元着色器,比如片元着色器我们可以选择phong shader,texture shader,甚至是简单的将每个片元设置为不同的颜色。
下一个阶段是几何阶段,这个阶段的输入是顶点数据,输出是屏幕空间下的顶点位置,具体流程是顶点数据输入,经过MVP变换(模型变换,视图变换,投影变换矩阵)然后转到裁剪空间下,顶点着色器就包含了MVP变换,也仅仅包含了MVP变换,当我们变换到裁剪空间下之后,接下来要进行裁剪,裁剪的目的是将在视锥体外的顶点和三角面进行裁剪,这样可以优化性能,在此步骤之后,我们将进行透视除法,在进行透视除法后,我们就将顶点变换到了NDC空间下,最后我们在进行视口变换,顶点就成功变换到了屏幕空间下,这就是几何阶段要做的事情。下一个阶段是光栅化阶段,在此阶段我们的输入就是上一个阶段得到的屏幕空间下的坐标,输出实际上是一张颜色缓冲图,具体阶段是当我们将顶点数据传入到光栅化阶段后,我们会先进行一个图元装配,将这些离散的顶点转换为三角形,然后我们会遍历这些三角形,我们首先拿到一个三角形,先进行背部剔除(第一次舍弃,如果三角形处于我们视线的背面,则舍弃),然后计算这个三角形的包围盒,然后得到这个三角形中覆盖的所有像素(第二次舍弃,如果点不在三角形内,则舍弃),然后对这些像素进行处理,通过重心插值的方式,得到每个像素的插值属性,然后进行深度测试(第三次舍弃,如果此像素点的深度值大于原像素点的深度值,则舍弃),在经过深度测试后,我们就能调用片元着色器了,片元着色器会告诉我们每个片元的颜色值,我们就可以把这个值写到颜色缓冲图中,最后再借助opencv的api显示到屏幕上即可,这就是完整的渲染管线流程。
架构步骤:
注意:本篇不涉及矩阵的数学推导,只给出如何架构项目
首先,根据渲染管线的几何阶段,我们要输入顶点数据,那么我们就要保存顶点的信息,你应该需要一个triangle类,这个类中的属性有顶点坐标,法线,纹理坐标,颜色信息,并且给与相应的set方法,来设置顶点。
Triangle.h

然后,我们在main方法中来导入obj模型,这里我们需要用到obj模型导入器,我们就借用下games101中的obj模型导入器,这里你可以直接copy代码,这里不重要
Main.cpp

这里我们使用了一个std::vector<triangle*> 类型的容器,方便我们存储每个三角形。通过这一步,我们就得到了所有的顶点数据,接下来我们就可以将这些数据全部输入到几何阶段中。
那么我们就需要定义一个顶点着色器vertexshader,这个类能够给我们提供MVP变换矩阵。
Vertexshader.h

模型变换矩阵:
作用是从模型空间变换到世界空间,这里我们先使用单位阵即可,就是假设我们的模型已经处于世界空间
Vertexshader.cpp

视图变换矩阵:
作用是从世界空间变换到视图空间
Vertexshader.cpp

投影变换矩阵
作用是从视图空间变换到裁剪空间
Vertexshader.cpp

这样,当我们创建一个vertexshader对象时,我们就能够获取到MVP矩阵对顶点进行变换。
然后,我们就可以来实现渲染管线,我们需要一个rasterizer类,我们将在这个类中实现完整的渲染管线。我们将在此类中定义draw方法,draw方法将完整模拟整个渲染管线。
Rasterizer.h
![]()
Draw方法将接收trianglelist,也就是将所有的顶点数据都输入到draw方法中,后面的angle参数可以先不用管。
这样,我们就完成了输入数据的步骤,下一步就是对顶点进行变换,我们已经定义了vertexshader类,这个类中已经包含了MVP矩阵,所以我们只要在rasterizer类中引入一个vertexshader类的对象即可
Rasterizer.h

在rasterizer类中,定义此成员,这样我们就能调用vertexshader中的矩阵
Rasterizer.cpp

这就是draw函数的实现,rotation_matrix矩阵是为了我们能够对模型进行一定的旋转。
重要的还是看MVP矩阵,我们一般使用的都是左乘矩阵,这里注意一下,rotation_matrix如果乘在model_matrix左边就是对模型进行旋转,如果乘在view_matrix左边,就是旋转摄像机了。
然后进行了一个for循环,for循环将遍历每个三角形,newtri就是我们提取出的一个三角形。
我们对拿到的三角形的顶点位置乘MVP矩阵,这样就能够得到裁剪空间下的坐标。
这里注意一下,为什么我们要保存view空间下的坐标,这是为后续背部剔除和phong shader中我们求光线方向和视线方向做准备的。所以一般来说,我们会保存下视图空间的坐标。
Rasterizer.h

我们保存了view_pos的位置
这样,我们就完成了MVP变换,根据渲染管线,我们将进行裁剪,但是在这个项目中我们先不去实现裁剪方法,因为裁剪实现还是挺麻烦的,当我们输入一个三角形后,经过裁剪,最多能产生9个顶点,这样一个三角形就变成了7个三角形,因为4个顶点能组成两个三角形,以此类推
那么继续进行下一步,我们要进行透视除法,这个就是将三角形的x,y,z都除w即可
Draw方法中

同理,下一步将NDC空间转换到屏幕空间

到此为止,我们已经完成了几何阶段,我们已经得到了屏幕空间的坐标
下一阶段是光栅化阶段,首先进行的是图元装配,将离散的点组成三角形,其实这里我们已经做了,因为我们一开始就是用三角形存储的,而不是离散的点,实际上正规的做法是我们先得到离散的点(经过裁剪后的三角形),然后在此处再重新组成三角形

然后,从三角形像素遍历到得到整个颜色缓冲图我们都在raster函数中进行。

根据渲染管线,我们要先得到三角形的包围盒,然后在包围盒内遍历每个像素
SSAA是一种采样技术,这里可以先忽略,将ssaa_factor忽略掉

求包围盒就是取三个顶点最小的x和y,最大的x和y,然后组成的一个矩阵,这里也好理解
确定包围盒之后,我们就可以遍历像素

X,y代表的是像素的左下角,加上0.5后表示的是像素的中心,注意看三次舍弃,先背部剔除,然后判断像素点是否在三角形内部,最后进行深度测试,这里看着可能有点混乱,主要还是我比较懒,不太想改,这里你们把get_index_ssaa()当成get_index(),把ssaa_depthbuffer改成depthbuffer, 把ssaa_colorbuffer改成colorbuffer就行了,完成舍弃掉ssaa就行
这里有一个新的类framebuffer,这个类就是用来存储colorbuffer和depthbuffer的

在rasterizer.h中我们也要创建这个成员

Fragment_Payload结构体用于保存插值属性

Fragmentshader就是片元着色器,可以定义每个片元的颜色,这里我用了两个shader,一个是testshader,一个是fragment_phong_shader。

核心渲染管线到这里差不多就结束了,最后这块比较难讲,你们不如直接去看我的源码,自己一琢磨应该就差不多了,这篇文章主要讲的是软光栅如何去架构,如何分文件,没讲函数实现的数学细节,如果我说的有问题,或者你们有不懂的问题欢迎指出。
2294

被折叠的 条评论
为什么被折叠?



