小熊挺喜欢玩游戏的,对于游戏画面有所追求,记得高中第一次玩战地的时候,惊叹于画面细腻的表现,并且还能开坦克车,这样的事情深深吸引了我。我是一个画面党,为了追求更好的画质表现我开始研究设置面板里各个选项的含义,但是并不是特别深入,上了大学幸运地拥有了时间(主要指可以逃课)让我可以重新更加深入的了解图形背后的技术原理,以及那些让人痴迷的图形算法,真是行云流水让人拍案叫绝。
所以做了这样的项目,让我对背后技术有了更深理解,和更好使用Unity或OpenGL了(
得益于前人的工作和巧妙的思想,我在阅读了这些文献后,融合了自己的想法和小思路,做出了这样的作品。虽然现在渲染器已经有很多并且五花八门,但是拥有一个属于自己思路和想法的渲染器还是挺让人有成就感的。
本软件渲染部分实现只使用了C#和C++自带的一些STL库。并没有使用OpenGL或者Direct库。
接下来的文章将会尽可能的表明外部引用资源的出处,有些是互联网的资源所以并没有找到源头,但是我会将其标注出来。如果有什么地方没有标准或者错误标注,欢迎在评论区为我指出。
在本文最后,将会给出更详细的资源使用列表。
本文不允许转载发表,一方面这是小熊的日记,写在自己的博客以此做一个纪念,另一方面本文还有很多部分尚未改善完成,待来日方长,再让我慢慢对你诉说。
接下来的文章介绍的部分,除了特别标注以外,是由我所完成和实现的。
一个高性能,低占用,便于用户使用的CPU离线渲染器。
本项目不使用现有的相关图形库(如:OpenGL、DirectX)而是从头自己写一遍(根据现有的论文),重新设计相关算法与技术。它具有高性能,低占用的特点,它便于用户使用,采用离线算法进行渲染计算。
在这个渲染器项目中,实现了众多算法包括不限于NPR基于物理的真实渲染,PBR非真实渲染等;可以配合现有的Blender、C4D、3DMAX等DCC软件,进行协同使用;它操作简单,支持简单化云计算等创新功能;完全使用CPU进行渲染,对GPU做到了0占用。
实现了能够渲染一些画面和导出一些动画等功能。
(图中像素女生来自游戏《杜若花开》,一款大二写的Unity像素风手游,设计参考于美术@鱼鱼@小勤@小满@盖子同学)
(Texture和Material类设计图(部分))
如上图所示,小熊设计了一些类如Texture来表示基础的纹理材质的模样,然后这个类派生了其他的可能纹理,用来表示材质贴图。其中设计了uv这样的变量来采样图片中的颜色信息。
而Material类则用来描述物体的表面属性,大体可以分为光滑或不光滑,粗糙的,金属类或非金属。目前研究到了这里。
(图:自己的管线流程设计稿,上图部分摘抄自{参考文献-1})
设计了一个比较简单的渲染管线,主要就是用来模拟一些顶点和片元上的操作,最上面的图是常见的渲染流程图。
上图就是一个简单的示意图了,小熊的项目就是这样子进行运算的,而描边等算法我使用了OpenCV等功能库进行制作,主要是比较简单。
同时设计了这些小熊还设计了一个可以解耦合的渲染接口,用户只需要实现接口就能放入到渲染器之中进行使用了。
采用C#和C++混合编写策略,C#部分手动控制GC回收,提高软件部分的性能,C++负责编写一些需要结合OpenCV或者高性能计算的代码部分,并把C++部分编译为DLL,供前端C#调用。为了提高性能,完全发挥出CPU的实力,采用多线程的软件实现思路,设计一套无锁的内存区域。
C#部分同时负责编写ASP.NET服务器,用于做分布式云计算的基础。
在这个部分,同时也实现了一些数学算法,PbMath,用于做数学的线性变换和矩阵运算等操作。支持矩阵于向量的乘法,支持常见的点积与叉积。
public static Vector3d operator *(Matrix3x3d matx, Vector3d vector) {
return new Vector3d((matx.e[0] * vector[0] + matx.e[1] * vector[1] + matx.e[2] * vector[2]),
(matx.e[3] * vector[0] + matx.e[4] * vector[1] + matx.e[5] * vector[2]),
(matx.e[6] * vector[0] + matx.e[7] * vector[1] + matx.e[8] * vector[2]));
}
下面本文章将介绍这个项目的核心理论,这部分参考了一些经典的论文和图形上的实现。具体请见文章的论文/资源引用声明或者文末的附录部分。
我(小熊)参考了这些前人所作的工作,在这些人的基础之上,完成了这个项目。
这部分介绍了本项目实现的理论基础。
开放端口,然后接收POST和GET请求。使用MVC设计模式,虽然View部分很简陋,只用于检测Api能否发送,用了Swagger框架。
接收渲染任务和分配渲染任务,调整建立连接的计算机相关的计算任务。
云计算流程:【接收到连接】-【接收到渲染任务】-【向连接的计算机发送渲染任务的场景】-【对每台计算机设定渲染的范围,然后开始】-【一直重复上一步直到没用画面可以分配】
用过DCC软件估计都知道,模型是可以被赋予各种不同的材质球的,我的实现是这样。
如上图,给每一个三角形图元配置不同的材质地址,当然光线求交的时候就带上材质的地址,这样就能够实现一个物体带上了多个材质,使用了指针进行实现。
部分参考文献:
- 《计算机图形学原理,第四版》.
- 经典的图形学虎书
- Games101,Games102和一些其他文章博客
光是一种电磁波。不同波长的可见光投射到物体上,有一部分波长的光被吸收,一部分波长的光被反射出来刺激人的眼睛,产生了对于色彩的感觉。
物理上不可分割的光有红、橙、黄、绿、蓝、靛、紫等七个颜色。事实上,小熊渲染器并不打算直接模拟这些光的波长颜色,我直接使用了Red、Green、Blue这三种颜色,进行模拟。
模拟光学折射率,直接使用数学中的乘法原理,由于光拥有干涉,衍射等特性。所以真正投射到物体上的光源应该经历了多次反弹,以下的公式出于一种简单的模拟,绝非真正遵循了物理的规律。
因为
( 0.3 0 0 0 0.2 0 0 0 0.5 ) ∗ ( 1 1 1 ) = ( 0.3 0.2 0.5 ) \begin{pmatrix} 0.3 & 0 & 0\\ 0 & 0.2 & 0\\ 0 & 0 & 0.5 \end{pmatrix} * \begin{pmatrix} 1\\ 1 \\ 1 \end{pmatrix} = \begin{pmatrix} 0.3\\ 0.2 \\ 0.5 \end{pmatrix} 0.30000.20000.5 ∗ 111 = 0.30.20.5
这样的线性代数过程可以看为,投射中,损失了一些光的能量。
V c o l o r = V 折射率 ∗ ∏ V c o l o r (光线折射) V_{color}=V_{折射率}*∏V_{color}(光线折射) Vcolor=V折射率∗∏Vcolor(光线折射)
对于上面这样的公式,V是Vector3d类型,折射率是存储在计算机类里面的一段数据,是的,这也是一种递归,光线折射拥有一个迭代深度的限制。
模拟真实世界物理表面,最主要的是要模拟两大物理光学方向,一种是折射,一种是反射。
在现实中,物体表面并非绝对光滑,因为表面在微观层面上,凹凸不平,所以材质展示了漫反射效应(Diffuse)和金属反射效应(Metal)。如上图。对于光的波长而言,物体的表面尺度对比它是很大的,所以可以将每一个物体表面看作一个很小的平面,这个平面上拥有法线,这样光线的各种计算才有了理论基础。这是小熊对于这方面的理解。
迭代式光线追踪又称递归式光线跟踪算法,通过模拟光线反射与反弹,进行下一层次的迭代计算。相比于过去,光线投射渲染的进化发生在1979年,Turner Whitted在光线投射的基础上,加入光与物体表面的交互,让光线在物体表面沿着反射,折射以及散射方式上继续传播,直到与光源相交。这一方法后来也被称为经典光线跟踪方法、递归式光线追踪(Recursive Ray Tracing)方法,或 Whitted-style 光线跟踪方法。
(上图是互联网上下载的)
我们想要得到的结果是,光打在物体上,得到的颜色样子。但是从光源点进行投射,由于光的方向性,很多光线并无作用在物体上,由于光路具有可逆性,所以本算法从屏幕表面进行投射。然后反向求解光源信息,如上图所示。
同时,我发现,光线在投射的时候,有时候会浪费迭代的机会,因为投射的都是黑色的部分,所以我想接下来再次改进,阅读一下文献,让光线投射的时候尽可能的打到有光源颜色亮度的地方上去,这样可以避免浪费,和降低时间复杂度。
下面是,本项目这个部分的相关伪代码。
1. Vector3d Ray_Color(Ray ray, HitTable world, int depth) { //投射光线
2. 到达迭代次数则回退;
3. if (world.Hit(ray, 0.0000001d, 0x3f3f3f3f, out hitResult)) {
4. 开始下一次计算渲染,如果支持的话;
5. } else {
6. return emitColor; // 返回自发光的颜色
7. }
8. }
9. // 返回背景颜色
10. return GobVar._BackColor;
11. }
程序实现这些光学模拟,参考了一些经典算法,简单来说,如上图所示,通过对物体表面法线方向的随机模拟光线投射样子,进行相关的物理演算。
此过程事实上是一种积分的运算,数学描述如下:
C o l o r = ∫ [ L i ( w i , x ) c o s ( w i , n ) d w i ] Color=∫[L_i (w_i,x) cos(w_i,n)dw_i ] Color=∫[Li(wi,x)cos(wi,n)dwi]
以上积分大体可以理解为,对于一个物体表面的点,它累计的光影响值是多少,本项目根据MC蒙特卡洛积分算法,对以上公式进行了如下的编写(也是函数的编写)
C o l o r = 1 n ∑ j = 0 n L i ( 不同的方向 ) P ( 不同的方向 ) Color={\frac{1}{n}}{\sum \limits _{j=0} ^{n} \frac{L_i(不同的方向)}{P(不同的方向)} } Color=n1j=0∑nP(不同的方向)Li(不同的方向)
本项目支持调整以上公式的n的部分,当n越趋向于无穷大的时候,那么结果将会越来越准确。
以下是使用项目,进行相关渲染的画面截图。通过设定,表面的光滑程度,进行相关的计算机图形学模拟,效果如下。
(上图的天空HDRI文件为互联网上下载)
通过设定粗糙度的值,从0(光滑)到1(不光滑)。可以看到下图这样的结果,符合现实中,物理光学上的表现。光滑的物体能够反射更多的场景灯光,不光滑的物体表面反射不了一些场景的信息。本项目基本实现了这样的效果。
(上图背景HDRI文件为互联网上下载)
参考文献:
- 刘乐乐的那本UnityShader
- 网上的博客和文章
- 下面的图片来自游戏《塞尔达》,知乎上copy的图片,还有一些来自WIKI。
通过手动模拟高光反射,色差控制,阴影光照,以及一些轮廓光照来实现。
Phong算法又称裴祥风算法,通过计算机物体表面的高光,阴影,环境光,折射率等,得到结果。此算法时间复杂度较高为 O(N+M) 但是实现较为简单,所以应用广泛。
在卡通化渲染中,并不需要更好的物理效果,所以选择了此算法。
如上图,我们通过表面N法线,与光线方向L和眼镜方向进行相关的计算,而H则是一个半程向量,为V和L的中间那个向量,用于计算高光。
(以上两张图摘自WIKI百科,对于PHONG算法的介绍)
我们实现卡通化渲染是使用Phong光照进行计算,计算完毕后使用OpenCV来进行颜色差值化(将颜色映射为两个颜色,这个部分正在研究),然后使用OpenCV进行描边检测,检测完毕后绘制边缘。这样一张卡通化渲染的图像就渲染完毕了。
(小兔子的模型请见本文末的资源引用)
这是卡通化后的结果,渲染完成后检测图形的边缘然后进行描边和一些后期处理。这部分在C++代码部分完成了。
参考文献:
Games101等系列,还有网络上的博客和文章
法线贴图是一种将模型的切线空间下的法线位置保存在一张贴图里的技术,使用这个技术,可以使得在不增加顶点的情况下,然后模拟出顶点很多的模样,是一种视觉上的Trick。
这个部分应该有一些介绍图的,但是懒得去切换APP了,请自己去网上看一下吧。
(上图法线贴图于漫反射贴图,下载自网络)
通过转换切线空间的位置抵达世界空间,然后再去进行相应的计算。
通过设置不同的尺度来控制法线贴图的比例大小可以构造出不同的落差感。从左到右分别是法线贴图的:0x,01.2x,1.5x尺度。
简单来说就是因为图形本身应该是连续的,但是采样是离散的,那么有些信号就采样不到了,而发生了混叠。就是频率太低了嘛~那怎么办?那就提高一下采样的频率,但是如果采样频率太高了那么就又会发生代码运行太慢的情况,所以要在重要的边缘上进行采样。
注意观察图像边缘,再多次采样过后,图像边缘变得模糊但是同时也变得更加平滑,这是因为它采样了其他像素点的信息。
光线求交在整个渲染流程中,是一个很大的时间占比(后文将有详细的测试报告),通过加快光线求交速度,将会有一个显著的提升。
单纯遍历内存中所有数据时间复杂度将是O(N)。如果构建BVH二叉树,则可以把时间复杂度将为O(Log N)级别。BVH算法核心是,分治算法,构建一个包围盒,左右两个包围盒再去递归。
原理是随机选取一个轴度(XYZ),将物体排序后,从中间分割开。但是这个算法有一个很大的问题,那就是,如果物体进行移动,那么就需要重新建造模型,根据《Ray Tracking In One Week》系列书籍中,提到的相关算法,我们可以只进行移动光线,而不移动模型,这样的优化思路,又成功地,让我们的项目支持了,渲染物体移动,旋转,渲染动画等功能。
这些东西说创新我都有点不太好意思,因为它们实在太微小了,甚至有一些我还没有研究完毕。不过我先把我的内容先发在这里,等我以后再去慢慢地研究。
首先渲染一张红黑图,然后再在这个红黑图的基础上填充像素的颜色。通过划分出哪些区域需要更大的采样,哪些区域只需要更少的采样,以此来更快速的出图,并且保证质量不算特别的下降。
目前这方面我正在研究一个自适应的公式来调整采样频率的大小,此部分有待进一步的研究。
可以看到画面开始慢慢得根据红黑图的轮廓进行着色计算了。
(本部分的人物模型见本文末资源引用部分)
这个就是用来保存自己下面的模型文件和摄像机位置以及贴图信息的文件了。它支持Hash校验,将Hash校验码直接保存在了文件的内部。
这个文件格式内部使用了XML来描述,然后本项目会读取这个文件,和保存这个文件,并且云计算中传递信息的用的就是这个文件格式。
这份文件与以往文件还有不同的地方是,它支持持久化的存储,比如现在有一个操作,修改了模型顶点位置,传统的Obj格式文件就要保存两份了,而我们的PbObj格式文件会差异化保存,然后去恢复出上下两个步骤的操作以及模型文件的样子。
从原理上来讲,这是行得通的。
1. <SphereXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
2. <objName>SkyBox天空光照盒子objName>
3. <needRender>trueneedRender> // 渲染可见性
4. <center>
5. // 省略…
6. center>
7. <mat xsi:type="SkyMat"> // 保存物体相关的,材质信息,也可以保存数组中的引用列表
8. <mTexture xsi:type="Solid_Color">
9. // 省略…
10. mTexture>
11. <_tex xsi:type="Solid_Color">
12. // 省略…
13. _tex>
14. mat>
15. SphereXML>
16. <History>
17. 这里通过记录每一次操作实现上下过程持久化
18. History>
写完了代码当然要玩一会,说错了,是实验一会,所以进行了一些相关的实验测试,下面是本项目的实验测试报告。
以上是Cornell_Box的实机渲染画面(左),CornellBox由康奈尔大学在SIGPRA`84中提出,用于测试程序渲染的结果
是否正确。正确的结果应该如右图,场景内的小盒子上反射出墙壁的颜色,而地板因为头顶光照则产生了一些黑色的阴影。
最主要的是研究,光线的明暗变化,以及光线的漫反射,直接光是否计算正确。对比上图中的光线细节和模型产生的阴影可以知道,本项目的模拟是基本上正确的。
这张照片拍摄于学生宿舍阳台。人物为我们自己研发的软件《杜若花开》中的依依。我们通过3D打印技术,将游戏中人物在现实中复刻,由于喷漆缘故,人物并不是完全漫反射(右图),在计算机中,我们模拟了完美漫反射效果。
使用了Phong模型的光照和对光源的特殊处理,得到的结果。
(中间图片来自:央美@小袁(袁宇静)同学)
通过对比如上图中间和右边图片,我们的软件成功模拟了卡通手绘效果。
使用了OpenCV的双边滤波,主要是想要保存模型的边缘信息,所以对于这种色块颜色插值不大的就做了降噪处理。
左图是降噪后的,右图是降噪之前。
(各个函数在CPU中占比时长)
通过我们的技术分析,我们发现我们程序大体上有效的利用了CPU多核心的优势,并且最占用时间的函数是AABB.Hit函数,这是在判断,光线是否和场景中的物体相交。
我们程序完全不使用显卡进行工作,只占用内存(RAM)而不占用显存(DRAM)。证明如下:
任务管理器(GPU零占用)
内存占用数据
从总体来看,我们的渲染器内存占用小,持续渲染内存占用平稳。GC回收平顺介入及时。上文两张图片是在716个三角形,迭代100次,MSAA4x下,进行测试的,测试机详细数据在下文。
为了对比不同场景下,不同软件下的内存占用数据,我们做了相关的测试,数据汇总如下。
渲染图像 | 参数信息 | 内存占用 |
---|---|---|
一个简单球体 | PowerBear软件(本项目) | Min:67.2mb Max:69.4mb |
一个简单球体 | Blender软件 | Min:720mb Max:821.2mb |
依依(717个三角面) | PowerBear软件(本项目软件) | Min:79.1mb Max:82.4mb |
依依(717个三角面) | Blender软件 | Min:802.1mb Max:874.2mb |
简单球(本项目) | 简单球(Blender) | 依依(本项目) | 依依(Blender) |
---|---|---|---|
通过对内存占用的详细测试,我们对比了任务管理器中的内存占用数据,我们得到了初步结论,在性能不好的机器下,本项目的渲染器占用内存小,适应性高,具有一定的成本使用优势。
环境变量控制如下,画面大小:500*500px,采样次数:100spp,开启动态采样,开启多线程,如果有支持CPU计算插件,尽可能使用CPU而不是GPU进行运算。降噪关闭。
我们渲染器,在支持GPU渲染的软件对比下,速度上并无优势,部分大场景中,渲染速度显著慢于传统渲染软件的速度。这是因为GPU能够很好的处理并行任务,而CPU更擅长处理逻辑复杂的任务。
单纯遍历内存中所有数据时间复杂度将是 O(N) 。如果构建BVH二叉树,则可以把时间复杂度将为O(Log N)级别。BVH算法核心是,分治算法,构建一个包围盒,左右两个包围盒再去递归。我们对比了,不够建包围盒,和构建包围盒的时间占比,数据如下。
实验环境,随机小球,2X 20采样,多线程核心数:8 CORES,摄像机坐标:(0,3,1)(0,-0.59999,-1),场景球体数目,随机大量小球:604元素,很大的场景:30w+面数
通过BVH将场景里面物体按照一定顺序划分,进行建立二叉树,从而加快了场景光线求交的速度,经过优化,从原来的 O(N) 级别复杂度成功降为了 O(LogN) 级别的复杂度。通过实验可以知道,在大量物体下,光线求交速度比未经过优化的场景更加快速。如果场景物体本身比较少,那么BVH并无显著加快对于常规光线求交算法而言。
我们发现,部分画面区域,提高采样次数,无显著意义。如上图左边所示,红色部分应该提高采样次数,而黑色部分不需要提高采样次数。
我们将舍弃那些,提高采样次数也没有太多作用的画面区域。这样来加快整个渲染画面的流程。规定渲染参数如下:50spp,500*500px,开启多线程。数据汇总如下:
可以看到,通过动态采样算法,屏幕占比越小的情况下,渲染时间长度越短,对于复杂的屏幕渲染,动态采样算法发挥的作用较小,但是对于屏幕占比不大,场景简单的情况,渲染时间显著减少,具有一定的意义。
测试场景,包含天空HDRI,一个金属小球和一个漫反射小球。采样为20次,光线反弹50次,画面大小500*500px。纹理大小1K分辨率。
经过测试多核心渲染速度是4.32秒,而单核心为11.424秒才渲染完毕。多线程速度比单线程速度快了3倍。
这部分介绍了一些其它和本项目相关的内容。
本文章的一些实验素材引用了互联网上的一些资源,现在介绍如下:
还有一些天空贴图,墙壁瓷砖贴图引用自互联网,没有找到作者源头。
也就是小熊同学自己的电脑了。
部件 | 型号 | 性能 |
---|---|---|
CPU中央处理器 | AMD RYZEN | 6800HS |
RAM内存 | 三星M425R1GB4BB0-CQKOD | DDR5 4800MHz 8+16GB(24GB) |
ROM电脑存储 | 西部数据 WD PC SN735 SDBPNHH-512G-1002 | 512GB PCIE4.0 |
电脑主板 | 华硕GA503RM | OEM:ASUS - 1072009 |
以上,测试数据如无特殊说明,均为这里列出的测试数据。由于本项目最主要看的是CPU性能,内存延迟和SSD读取性能,故其他数据暂未详细列出。
软件名称 | 版本 |
---|---|
POWERBEARRENDER渲染器 | 截至2023年10月版本,内测 |
BLENDER | 2.81 Windows |
CINEMA4D | R20 Windows |
很开心的说,这个项目的初始想法,和最后的编程实现来自于我自己。并没有特别依托学校和老师的资源,孩子总要长大,所以要有动力和勇气自己尝试写一下代码。
不过话是这么说,我依旧要很感谢我在周围所遇到的所有人,我的同学帮助我,在老师来的时候喊我起来,这样让我可以在课堂上开小差看这些喜欢的内容。有时候睡过头了同学也会喊我去上课,让我不用天天写检讨,这是非常nice的时刻。同时我也遇到了很棒的队友和同学,他们愿意与我组队并且做了很多有意义的工作,他们帮助我写了经济效益预期,调查问卷等等内容。如果没有他们的存在,让我去写和做这些调查报告我是极不乐意的。
我也要感想我所遇到打指导老师,虽然我们学校没有计算图形这个方向(毕竟只是一个三类本科民办),但是他们为我开阔了视野。其中有一位祝老师让我从外语系转到了信息系,我要感想她对我的发现和认识,其中我的薛老师为我指点了很多很多在计算机视觉上的内容,并且帮助了我打通了一些基础的OpenCV操作和人工智能的基础姿势,这是我所开始的开端,当时也和老师一起探讨过我们是否应该使用Qt来进行UI界面的编写,不过我比较喜欢C#语言想要减少一点C++的占比所以最后我使用了WPF,哈哈。其中的祁老师为我的人生做了很多的建议,并且在我失落的时候鼓励我,开导我且给了我许多中肯的指南。
我也要感谢先前的人,他们做了非常优秀的工作,且无私的将这些成果公布了出来,让我有机会能借助他们的思路,实现了自己的项目。
(我估计应该没有人会在我的基础上继续工作了吧,毕竟这个就是一个自己写着玩的小工具,所以就不感谢后来者了,嘿嘿。)
就算写道这里还是有很多的东西让我尚未完成,比如说一些更好的算法,体积光照系统,更加先进的降噪,还有我的红黑图的继续下一步的改进和优化。
前途路漫漫,等以后做了更多工作就来这里再写一篇随笔好啦。
大概算了一下,编写这个项目大概花了10个月的时间,几乎一有空我就会去写它。
管它有什么用途,有什么可以得到的,总要有机会为了自己所喜欢的东西去为之努力一次,这份旅途,它至少不亏啊,我看到了内在的东西和风景,我相信它是值得的。
下图是我在高中时代所做的作品。
发布时间为:2019-07-04 11:36:28
这个视频使用了UE4进行制作和渲染。
而下面这行图则是现在这个项目所输出的画面
发布时间为:2023年10月13日
由自己的渲染器所输出的画面。
时间跨度为4年,谨以这份图像,纪念我所经历过的风景。有人说时间是最好的告别,而让我还在继续研究的原因仅此就是一个:我喜欢。
希望未来的我能继续保持对技术的喜欢,继续下一步~