0
点赞
收藏
分享

微信扫一扫

关于在寒假用两周从零手写包含模拟着色器的软渲染器这件事

轮子哥说过,编译原理,操作系统,图形学是程序员的三大浪漫,既然以后想从事游戏方面的工作,造这个轮子是不可避免的。其实早在本科的时候我就有写个软渲染器的想法,不过已是大三,变得佛系了起来,最终也没有实现。不过今年自我感觉状态良好,哈哈哈哈,应该有能力完成这个轮子了。

先贴上代码链接:

GitHub - FREEstriker/AirRenderericon-default.png?t=M0H8https://github.com/FREEstriker/AirRenderer

因为今天中午才将将完成,文件并没有整理,readme也没有写,之后会整理规范一下文件。目前是完成了第一个阶段,包括:基本的树状对象、相机灯光组件、纹理采样、模拟的顶点着色器和像素着色器、forward渲染流程、基础的工具类,并使用以上组件,完成一个多灯光多物体包括漫反射纹理与法线纹理的渲染。之后的阶段应该会进一步扩充管线,加入几何着色器、曲面细分着色器、更多样的光照、阴影、后处理与抗锯齿,并接入js作为脚本语言,计划研一下能完成吧,哈哈哈哈,感觉还是挺有难度的。

先介绍一下使用的库吧。向量矩阵数学库使用了glm,图像加载使用的FreeImage库,模型加载使用了OpenMesh库,桌面窗口使用了QT框架,其余都使用c++开发。

这个阶段最终的效果如下:

使用了法线纹理、漫反射纹理

只使用漫反射纹理

导出模型未使用平滑顶点

下面说一说代码的一些结构与我造轮子过程中遇到的难点,不保证全部正确,虽然好像看起来是没有什么错误的,并且图形学的第一要义就是看起来是对的那他就是对的,哈哈哈哈,不过如果出现错误的话希望能够指正。


关于树状对象

这个东西我是用的兄弟孩子树,写了一些简单的迭代器方便调用,这部分对我来说难度最大的其实是c++的语法,因为以前一直使用c#的关系,c++用的很少,而且考虑到通用性,想要把这个做成模板类,c#里泛型写的也不少,但是换到c++真实直接gg了,哪儿哪儿都报错,人都傻了,简直太难写了,而且居然要把头文件分出去,对不起,我真的喜欢c#。不过yysy,指针还是挺好用的,哈哈哈哈。

左孩子右兄弟二叉树,没啥好说的,非常简单:


关于组件化

组件化就是照着unity抄的,接口都一样,哈哈哈哈,不过把transform单独分出来了,并且transform的树状结构给了gameobject。由于没想好到底内部应该怎么实现,就干脆先用string做了个类标记,查找类标记再做指针转换,应该还有其他办法的,不过还没找。


灯光

灯光目前只有基类和点光和平行光,点光源抄的这个公式:

还乘上了一个窗口函数:

RTR4里都有。


相机

相机目前是正交相机,透视还没做,虽然只是替换个矩阵,不过透视矫正那里我还没有学,反正也组件化了,问题不大。这里有一个需要注意的地方,glm库的向量都是列向量,矩阵都是列主序的,所以写的时候记得转换一下。线代说实话我这几年学了好几遍,但还是不太行,只会基础的,复杂一点的根本绕不过来弯,难顶。


模型加载

本来想着自己写一个半边类,不过太麻烦了,干脆直接用了个OpenMesh库,这个库还是挺好用的,半边导航真的舒服,哈哈哈哈。选用模型格式纠结了半天,最开始用的obj,但是在用的时候发现,obj的顶点和纹理顶点个数不对应,而OpenMesh库里的顶点属性都是一一对应的,这就导致在三角面中纹理坐标插值的时候出现顶点使用了错误的uv值。

一个v有多个vt,但是库只能用第一个

我也没找到啥解决办法,索性就直接换成了ply格式,这个格式多存了顶点,所以纹理坐标插值结果是正确的。

一个顶点一个uv,舒服了


图像加载

为了方便之后的后处理和抗锯齿,我自己写了buffer和color类,但是图像并没有直接存在这里面,而是直接采样的时候直接调库,自己只需要计算坐标。选项分了这么几个:

mipmap的还没写,先占上坑哈哈哈哈。这几个本质上就是控制坐标计算过程的,没有任何难度,但是三线性插值还没学,以后补。而且采样的时候没找到库里有采样alpha通道的方法,真实离了大谱了,md估计还得自己在bitmap里找,鸡掰,早知道不用这个库了。


forward渲染流程

forward渲染流程是最基础的了,几个for一套,完事,哈哈哈哈。

但是既然做戏就要做全套,要模拟着色器这个流程还是太简单了些,所以在写的时候我其实最开始就确定了流程:

但是最后写出来和这个有一点出入:

不过差别不大,只是不用存每个顶点着色的结果了,省了一点内存,不过后面才知道根本聊胜于无。

在pixel_shading前要对顶点进行插值,使用到了重心坐标,我还是第一次知道,哈哈哈哈,学到了学到了。

不过我是直接抄的公式,根本没推导,哈哈哈哈:


模拟着色器

因为之后有计划实现其他算法,为了提高通用性,所以做了一个模拟的着色器类,现在只有顶点着色器与像素着色器,自我感觉还是挺好的,哈哈哈哈。

顶点着色器

插值

就是对坐标做了一些转换,之后会对每一个顶点的每一个数据进行插值,现在都是手写的插值,之后应该会改成更加通用的四元向量格式,统一插值。

这里要注意法线,转换坐标时要使用专门的矩阵,并且插值前后都要进行单位化,防止出现变形:

法线转换变形

正确方法

单位化防止变形

这里入门精粹和RTR4都有讲。

像素着色器里进行的计算比较多:

像素着色器

光照计算用的这个公式:

代码里的al、dl、sl分别就是环境光、漫反射光、镜面反射光,没啥可说的就是很简单。

这个里面最困难的其实是计算纹理法线的过程,以前就看过相关的,这次还是第一次仔细看明白了每一个过程。对象空间的法线纹理非常简单,直接采样后从颜色rgb转换成向量xyz就可以,毕竟是对象空间,哈哈哈哈:

对象空间

而一般使用的法向纹理都是蓝色的,比如我用的这张:

之所以是蓝色,是因为是在一个天杀的叫做切线空间的东西:

其实我感觉这东西叫uv空间似乎更合理一点,uv正好是xy轴,法线正好是z轴,而法线纹理存储的其实是对模型法线的扰动,一般来说扰动基本不大,所以这就解释了为什么纹理是蓝色的。

切线空间的转换需要需要使用到TBN矩阵,3D游戏与计算机图形学中的数学方法写的比较详细:

过程很简单,但是需要想一下计算的时机:既然每个像素都有通过顶点法线插值获得的法线,而且法切线、副切线、法线是正交的,所以应该只需要计算切线就可以通过其和法线的叉乘获得三个正交基了,根据上面的方法,切线应该是每个面都是一样的,所以每个面计算一次就可以了,我是选择把他传进了顶点着色器进行坐标转换,减少在像素着色器中的计算量,在像素着色器中使用叉乘和施密特正交法通过切线和法线重建副切线,从而得到TBN矩阵,将采样的切线空间法向量转换到对应空间中,这里我是转换到了观察空间,最后再对顶点插值获得的法向量进行扰动就可以了。

在实现过程中,思路队了基本没有什么特别困难的地方,但是最后扰动是我犯了一个非常nt的错误,我把采样法线和插值法线直接对应分量相乘获得了结果法线,结果渲染出来出现了诡异的错误,让我想了一整天,还把计算全部改到了世界空间,结果没有任何改善,麻了反正:

错误扰动

光照左侧完全错误,简直百思不得其解,本来不该有灯光的地方出现了光照,将法线纹理全部去掉后恢复了正常:

纯球体

鸡掰,检查了半天切线的计算,最后发现是采样法线和插值法线的扰动计算错了,应该是使用两个向量的对角线才对,麻了。修改后才正确:

球体凹凸感有了


缺点

完全没有经过系统性的测试,完全不知道哪里有隐藏的bug。

其实现在这个东西效率非常拉胯,编译时间超长、内存占用超多,一个球一个平面四个灯分辨率800-450,需要计算七八秒,并且占用450m内存,就离谱:

而且debug非常困难,面多得根本不可能定位错误的着色器位置,非常需要类似于Frame Debugger之类的工具,鸡掰。

文件未分类,每次找文件都得点半天:



展望

下一步就是增加几何着色器和曲面细分着色器,整理文件写文档,优化整个流程,加入js的V8引擎,脚本化shder,解决编译问题。

接着是阴影支持,和一些光照算法。

再下一步是增加抗锯齿,重点是MSAA,还有延迟渲染管线。

最终目标是一个SRP,哈哈哈哈。

希望能实现吧。

原神真好玩,我吹爆原神!

举报

相关推荐

0 条评论