Vulkan PBR与IBL实践

对我来说,每天能过得心情舒畅,有酒喝有美味佳肴吃,必要时工作一会儿,晚上睡得舒舒服服,就行了。 ——迪希亚

说实话我已经记不清上一篇文章是什么时候的事情了,感觉得有好几个月了,但其实我是在上篇文章结束就开始学习Pbr相关的东西了,只不过因为某些智障问题,导致我IBL的生成Irradiance贴图的部分一直是错误的,直到29号写完预滤波贴图的部分才发现自己前面哪里写错了,那是真的智障。不过好在赶在31号之前把全部的代码都写完了,也算是在去年完成的了。

还是这个仓库:

FREEstriker/Air_TileBasedForward (github.com)https://github.com/FREEstriker/Air_TileBasedForward

效果大概是这样的:

PBR+IBL - 知乎 (zhihu.com)https://www.zhihu.com/zvideo/1592840606350598145

切分的背景图:

生成的IrradianceMap:

至于为什么这么亮,我只能说原始的hdr图片里的那两个白光灯光照实在是太强了

生成的五级粗糙度的PrefilteredEnvironmentMap:

为了方便看,就放大了一点

生成的R32G32_SFLOAT格式的LutMap和压缩打包成R8G8B8A8_UINT格式存储用的LutMap:

踩到的坑

这几个问题真实困扰了好久,不过还好都解决了,哈哈哈哈。

半球采样

​用Hammersley进行半球采样的时候,抄错了。。。计算cosTheta的地方忘开平方了,导致算出来的IrradianceMap很奇怪,好像对也好像有点问题,看了两个月都没找到是这里写错了,一直是以为采样时空间转换哪里写错了,我真傻真的。

错误的IrradianceMap

就这,卡了一个多月。。。

QT的VulkanWindow

生成IrradianceMap的时候,我发现如果我像LearnOpenGL那样直接画一次就出结果的话,我的电脑会直接黑屏报显卡丢失的错误,似乎是占用显卡太长时间,导致系统以为显卡卡死了,就重置了,所以就只好使用分片的方式,一次只画一部分,分到几百帧里去画完。

但是到生成PrefilteredEnvironmentMap的时候,我发现如果要分片的话,最好使用一个StorageBuffer来存储Weight,这就需要原子操作了(但其实并没有必要,后面也没有用到原子操作)。但我在实际使用AtomicAdd时总会提示我打开拓展,但实际上我已经打开了拓展,并且通过打印支持的拓展可知,3060确实是支持atomic_float拓展的,非常奇怪。

开启拓展

支持拓展

上google查了查,发现似乎要设置一个VkPhysicalDeviceShaderAtomicFloatFeaturesEXT的结构体并且把它绑在VkDeviceCreateInfo的pNext上才行,但是QT并没有提供这样的接口,查了文档也没有。但我在QT6.4的手册里发现似乎有这个设置pNext的功能:

于是我把QT升到了6.4,然并卵,依然不能设置pNext,提供的setEnabledFeaturesModifier接口只能设置Vulkan1.0的Feature。我以为是我查手册的姿势不对,就又去翻QT6.4的源代码了:

​我只能说,毫无pNext的痕迹,所以如果想要使用atomic_float拓展,就只能改QT的源码再编译个自己的QT版本出来,我只是尝试编译了一下,反正6.4我是怎么都编不过,但是5.15编了一下午确实编完了,想想还是算了,干脆用个ComputeShader搞了一下Weight,后面会详细说,改QT什么的,还是留到下个大版本吧。。。反正现在也不是不能用。

FreeImage库

大多数Vulkan实现都是不提供RGB格式的,只提供RGBA格式,之前确实是问题不大的,基本直接就加载的png格式的带有a通道的贴图。但是要是加载EXR格式的HDR图片时,就有问题了,缺了个a通道,导致不能直接从内存拷贝到显存里,所以就需要先把它转换成RGBA在放在R32G32B32A32_SFLOAT格式的显存里。FreeImage库里确实存在这样的接口:

但是我从网上下载了许多的HDR图片,每一张在RenderDoc里查看它虽然是R32G32B32A32_SFLOAT的,可最大值都是1.00,完全没有HDR图片的样子,很是离谱,不管找多少图片都这样。但是去PhotoShop里查看的话确实是一张HDR图片,似乎是完全没有问题的。

没办法,就只能有去查手册了:

​好像说了,又好像没说,没办法,还是看源码吧:

​只能说真像大白了,我是没搞懂你RGBF转RGBAF还要搞个Clamp是做什么,RGBAF转RGBF也是一样的情况,挺无厘头的。

只能改了在编译一遍看看,去掉Clamp果然对了。。。

坐标系问题

虽然已经在前面的文章里每一篇都会提到坐标系的问题,但这次又出现了,我也没办法,我是真的麻了。刚开始的问题出在每次切分生成背景立方体贴图时,生成的6张图并不能组成向上面一样的十字图,有两张图总是需要翻转一下,剩下的图需要移动一次,很是离谱。

找了半天,发现大家用的世界坐标系的底平面都是XZ平面,而我由于高中留下来的习惯,我更习惯与使用XY底面Z向上的右手系,所以之前就都用的这个坐标系。而开启了VK_KHR_maintenance1拓展并设置后,NDC空间变成了和OpenGL一样的左手系,立方体贴图似乎就是照着OpenGL的NDC坐标系定义的。

我之前似乎是直接用片元的世界坐标系当做采样方向直接采样,生成时也是采用的类似操作,所以切分结果总是差点。之所以现在才发现这个问题,是因为我之前以为我用背景立方体贴图画背景的时候是由于Vulkan的某些差异造成的图片错位,就直接用PhotoShop修改了一下,满足显示需求就算蒙混过关了。

未启用VK_KHR_maintenance1的NDC

所以最后我把世界坐标系改乘了XZ为底面Y向上的右手系,而采样背景立方体贴图时,直接把片元的世界坐标的Z加个负号,这样就变成和OpenGL的NDC坐标系一样的了,直接这样采样就可以得到正确的结果了,且切分出来的六张图也可以拼成一张十字的图片。

Physical Based Rendering

虽然很早就知道有PBR这么个东西,但一直都没一仔细看过它的原理,反正就是很nb就是了。仔细看过之后,其实大部分都是已经确定的东西,只有少部分是需要推导的,反正最后的效果确实是比之前好了很多。

原理

​主要就是这个式子,等号右边的左半部分是漫反射,右半部分是镜面反射,需要了解的点包括立体角、菲涅尔、法线分布函数与几何分布函数。

立体角的部分可以看这个,写的很详细了:

漫反射部分是很简单的,kd就是漫反射所占的比例,一般就是用 来定义,c就是反射平面三通道的Albedo,之所以需要除以一个π是因为漫反射的能量会被分到法线半球上,最后的Li*(n·wi)就是把光照转换到法线方向上。

镜面反射的部分略微有点复杂:

F项就是菲涅尔项,大概就是下面这样的东西,视角固定时,光线越接近法线,反射效应越强,反之反射越弱。

​一般就用的是这个公式: 。其中,F0就是物体本身的反射率,所以应该是三通道的,是可以根据材质查表得到的,h是Light和View的中间向量,v就是View,有点类似于Blinn-Phong,大概知道是这样定义的就可以了。

​但是实际使用中,F0是通过这样一个式子所定义的: ,之所以是0.04是因为绝缘体的F0就是0.04,而albedo就是对应材质的反射率,通过一个metalness参数进行插值,就是比较灵活吧。

G就是几何函数的部分,由微平面理论可知,当光线入射和出射时,都会被遮挡掉一部分,几何函数所作的就是计算出最后出射的光线占入射光线的比例,少掉的光线就代表被遮挡掉了。

用的就是上面的公式,越粗糙,损耗的光线就越多,而最后将入射的有效比例乘上出射的有效比例,就得到了总的光照的出射比例。

D项是法线分布函数部分,这部分感觉非常有道理的样子,原理还是基于微平面理论:

当视线位置一定时,单位宏观平面内只有微观法向朝向h的微平面才会将入射光反射到摄像机里,法线分布函数基本就可以理解为单位宏观平面上微表面朝向h的比例,有了这个比例,就可以对光线进行衰减了。

总的来说,D、F、G这三个参数就是控制反射强度的,都是[0, 1],通过使用albedo、roughness、metalness三个参数来控制,很有道理的样子。问题就只剩下镜面反射部分的分母部分是怎么出来的了,这部分我是在这里学习到的,写得非常清楚:

具体物理单位相关的可以看这个:

我自己又整理了一下加强记忆:

实现

既然原理很明确了,实现起来就没有什么太大的问题了,而且之前本来就在灯光参数里加入了Color和Intensity参数,所以只需要改光照部分的GLSL代码就可以了。

shader接收的

Light基类定义的

但是偶然间我发现似乎GLSL的函数传参用的似乎是拷贝传参,这就很难顶了,因为我之前直接把一大堆参数不管有用没用全给传过去了,且内部经过检查灯光类型后,会进行一次分发,在使用一次拷贝传参:

​很浪费啊,感觉没必要,所以为了避免拷贝,我干脆就把PBR的计算用宏写了。

通用的部分就是公式的代码化:

通用的计算

不同灯光的强度需要做不同的处理,之前的文章已经说过很多次了,就是抄的RTR4里的公式:

不同灯光不同的计算

最后在用一个函数包起来,用来分发使用不同灯光函数,所以说基本就是去掉了一次拷贝:

没有太多可说的其实,和IBL比很简单。

在实际使用时,传入的Roughness和Metallic参数都是通过采样贴图得到的,并且最后还需要一个ao贴图来描述这个材质的自遮挡,所以需要一张三通道Albedo、一张单通道Roughness、一张单通道Metallic、一张单通道AO,这些贴图都是需要美术来提供的,所以我直接上网随便找了一个材质。

考虑到其中三张图都是单通道的,不如直接把这三张图合成一张三通道的图,需要考虑到的是,这三张图都是线性空间的,所以使用PhotoShop时需要一些额外的颜色空间操作才能正确的存储,最后合并出来的图就是这个样子的:

Roughness-Metallic-Occlusion贴图

最后再片元着色器里直接采样贴图传参就可以了:

Image Based Lighting

因为IBL也是用了BRDF,所以当时想着就一起写了算了,IBL的推导部分比较麻烦一些,实现起来也很麻烦。我是学习了LearnOpenGL上的教程,并结合网上的一些讲解完成的,原理推导主要是学习的这一篇文章,写的非常详细:

原理

拟蒙特卡洛积分:

本来的蒙特卡洛积分是这样的:

pdf是概率密度函数

所以如果我们要对法线半球做一些积分操作的话,就可以利用这个式子让它变成某种求和公式。至于生成随机样本,使用一个叫做低差异序列的东西,如果使用低差异序列生成的样本带入到蒙特卡洛积分中,这个东西就叫做拟蒙特卡洛了,大概就是这样。

生成低差异序列

低差异序列生成的半球样本是这样的

漫反射:

​观察等式右边的左半部分,c和π都是常量,没有问题。问题出在 上, ,其中 ,所以说 与h、v、metallic、albedo相关,在渲染时才能确定,所以最好把 给分到外面,预生成一称为IrradianceMap的图。但F中的h是与 相关的,需要把这部分做一个近似,所以就有了这样一个东西: ,相当于用 替换了原来的1,并且将coshv换成了cosnv,这样就可以将 完全搬出积分,只剩下对环境贴图的积分了,所以说漫反射的部分就变成了这样:

积分左边在实时渲染时确定,积分则使用拟蒙特卡洛对环境贴图预积分生成,实时渲染时根据片元法线采样就可以了。预生成时可以使用余弦权重的半球采样,也就是 ,所以上式就可以换成这样的形式:

所以预生成时只需要用N个样本来采样环境贴图,求和再平均就可以了。

镜面反射:

镜面反射部分用的是一个叫做spilt sum approximation的近似,把高光反射部分公式中的 给拆出去,成为这样的式子:

左边称为PrefilteredEnvironmentMap,右边可以将系数预计算到一张图上称为LutMap。

​至于推导中的pdf为什么要取这样一个值,我并没有深究,不过知乎上确实是有人讲这个东西的是可以推导出来的。这个pdf是与Roughness相关的,视角固定,不同Roughness的情况下,造成贡献的光线范围是有所差异的,大概是这样的感觉:

光线波瓣内的光线贡献光照

用这个pdf生成半球样本的话,就可以用Roughness控制波瓣大小了:

Roughness从0到1,从点到半球

而由于推出的 里面还有 、 这种东西,所以这里做了一种简化,在预计算时直接将采样方向上光线波瓣的值做积分,这样View、Normal就可以认为是一样的了。

简化模型

但在实际积分中,pdf确定的波瓣是H的范围,再通过View和H计算出入射的光线,从而得到光线波瓣,也算是微平面理论吧。

实际积分中的波瓣应用

既然Normal、View都一样了,就可以继续化简 了:

至于为什么 可以直接被 替换,似乎是因为F对结果影响不大,剩下的部分分布和值域也很类似,就直接换了。

这样就可以根据不同的粗糙度确定波瓣的大小,计算出一张PrefilteredEnvironmentMap,并且因为越粗糙就越模糊,正好和Mipmap很合得来,所以干脆就把每个粗糙度的结果存入对应的Mipmap就好了,实时渲染时直接使用 作为采样Mipmap的参数就好了。

去掉 就只剩下 这部分了,由于这个里面的 里面的F项带有很多实时渲染时才能确定下来的参数,所以需要对这个式子做一下整理,再把F项分到外面。

​在最后的A和B中除了 相关的样本,还剩下分母中的 、G中的Roughness,那么用这两个变量作为uv来绘制一张A在G通道、B在G通道的图就可以了,使用时直接根据 和Roughness来采样,在使用 进行计算就可以了。实际积分中也用到了前面提到的H波瓣计算光线波瓣的方法。

组合

有了上面出来的IrradianceMap、PrefilteredEnvironmentMap、LutMap,就可以在实时渲染中进行渲染了。需要根据法线N与视线V计算出可被观测到的光线L,因为漫反射部分预计算时是根据法线半球计算的,所以直接用N采样IrradianceMap就好了,而镜面反射部分在预计算时用光线波瓣做了积分,所以直接用L采样PrefilteredEnvironmentMap就可以了,最后根据 与roughness采样LutMap再合上 就可以了。最后组合出的公式应该是这样的( 、 的具体计算省略):

这样有了理论的支撑就可以开始实际的编码了。

修改框架

之前对VkImage与VkImageView的了解很模糊,只知道必须给Image绑一个ImageView才能访问,所以在封装Image类时就直接构造的时候申请一个VkImage和VkImageView,在生命周期内是不能够更改的,而且之前也没有用到Mipmap,无论是2D还是Cube一开始就确定了。

但是实际上,VkImageView和VkImage是“一对多”的关系。一个VkImage里会存有多张二维图片,一张称为一个Layer,每张图片还可以有多个Mipmap等级,不过每个一个VkImage里每个Layer的Level等级都是一样的。而VkImageView的作用则是设定以哪种方式来看这张VkImage。比如说:VkImageView0就只看Layer0-1Mipmap1-2,就是一个带1-2级Mipmap的长度为2的图片数组;VkImageView1就是单纯的一张二维图片;VkImageView2就是长度为3的只有第0级Mipmap的图片数组。

​所以我将封装的Image类里的一个VkImageView成员变量改成了一个以字符串为键的字典表,这样就可以通过视图名来访问Image拥有的VkImage的某个VkImageView了。

将VkImageView相关的信息都存入map中

通过视图名访问VkImageView

当想要对一个Image添加Barrier的时候,也是需要划定一个Barrier同步的VkImage区域的,我就直接偷懒使用上面提到的存储的VkImageView相关信息来填充Barrier的相关信息,所以要添加Barrier也需要添加一个对应的视图。

指定一个视图名,对象以资源添加Barrier

但其实这是不必要的,正确的做法应该是额外添加一个对VkImage分组的数据结构“ImageGroup”:如果需要添加Barrier就直接申请一个ImageGroup,只有相关信息并不生成VkImageView;如果需要一个视图,就先申请一个ImageGroup,再根据其中记录的Layer和Mipmap信息创建一个VkImageView。这样就不需要创建无用的VkImageView了。之后再改吧反正。

大概就是这样的感觉

除此之外,之前设计的资源系统也是有点问题的,除了Image这个类,还加了Texture2D的类用于加载资源,内部使用的就是封装后的Image,功能其实是重合了的,应该把加载用的相关信息放在AssetBase基类中,然后让Image直接继承AssetBase就好了,所以Material、AssetManager相关的地方就都改成了Image了,完全去掉了Texture2D这个东西。

直接继承AssetBase

考虑到IBL的步骤里可能会需要直接加载带有Mipmap的图片或直接生成一部分Mipmap,所以重写了Image的加载部分,并在json文件里添加了相关信息字段:

mipmapLayerSourcePaths控制需要加载的源图像路径

autoGenerateMipmap控制自动生成Mipmap

所以在加载时需要先把相应路径的图片加载到Buffer中,在拷贝到对应Layer对应Mipmap中,之后再根据设定使用Blit方法生成剩下的Mipmap,也就比之前多了两步而已。

先拷贝有源图像的Layer与Mipmap

再使用Blit生成剩余的Mipmap

后面应该会再完善一下。

切分立方体贴图

大部分HDR源图像都是一张等距柱状投影图,大概就是这样的:

而一般渲染时用的是立方体贴图,所以第一步就是先把一张等距柱状投影图切成六张的立方体贴图,便于后续过程的使用。切分时就使用一个fov为90°Attachment为正方形的相机,在原点上旋转朝向到六个面的中心,渲染一个立方体,就使用这个立方体的坐标作为采样的方向,至于如何采样等距柱状投影图,我是直接抄的LearnOpenGL上的公式。

抄来的公式

这样就可以直接开切了,需要切出来两种格式:一种线性空间的,用于画背景,需要做一个ToneMapping,直接存成png就好了;一种HDR格式的,用于生成IBL需要的其他阶段的源图像,单纯只是切分,我是存储成exr格式了。

生成的操作已经不属于实时渲染了,应该单独分出去成为一个资源生成管线,以在编辑器下单独使用,但这里我实在懒得分出来了,就直接在当前使用的Pipeline里添加了一个“BuildAssetRenderer”,并添加了一些RenderFeature用于不同资源的生成,所以直接将场景内的渲染器切换成“BuildAssetRenderer”就好了,但是窗口还是要显示东西的,所以就单纯的画个背景上去暂时将就用着。之后应该会重写一部分内容,把资源生成分出去成为一个资源生成管线。

添加BuildAssetRenderer渲染器

在BuildAssetRenderer渲染器内使用不同的RenderFeature生成不同资源

而且我也没有写图像存储的相关操作,主要是Vulkan不怎么支持RGB纹理,所以写起来会非常的麻烦,所以干脆就直接在RenderDoc里截个帧,把渲染到Attachment上的内容手动保存下来。。。

ToneMapping

至于ToneMapping,我是直接用的最简单的 。并且需要注意的是,RenderDoc里保存的是线性空间的图,所以加载时注意一下格式。

背景贴图

Hdr贴图

最后的生成结果应该是没有问题的。

生成IrradianceMap

和上面切分立方体贴图时一样,也是将摄像机转六个方向,每次渲染一个立方体,将立方图的片元位置直接作为采样的法向,然后在法向半球内运用上面的公式进行计算就好了。

由于用低差异序列生成出的采样方向是单位半球内的,所以需要将单位半球内的采样方向换算到法向半球中,之所以要额外计算一个up的原因是避免两个近似的向量做叉乘操作造成错误。

计算转换矩阵

还有另外一个问题,就是当单次计算生成的样本数过多,我反正是会黑屏报错,不知道是Vulkan的问题还是QT窗口的问题,所以我将计算分到了多帧上,一次计算一部分,这就需要一个东西来保存每次计算出的结果。观察一下这部分公式: ,直接把N拆开就好了,变成这样: 外边的是分帧,里面的是每次的样本总数。而存储的话,就直接使用Shader的Blend功能,设置成 就可以将每帧的结果累加起来了。

需要传入总步数、切分帧数和当前帧的索引

根据传入的数据还原出当前样本在总样本中的索引

半球采样

获取样本、采样、除以样本总数、设置srcColor

设置Blend参数

我直接狂采1048576次,呜呼!

生成PrefilteredEnvironmentMap

先观察一下式子: 显然不能像IrradianceMap那样直接切分,因为分母是相关的,所以最好分两步:先分帧累加分子,顺便把分子给记录下来;再将分子分母相除得到结果。我是用一个StorageBuffer来存储的分母的,在每帧的第0级Mipmap的第0面的第0号像素计算时将分母写入Buffer然后这一帧结束后因为已经对CommandBuffer执行了WaitFence了,所以下一帧也不需要同步资源,直接继续就好了。

所以第[0, stepCount-1]帧只做累加操作,且将分母累加入StorageBuffer。第stepCount帧做分子分母相除的操作。第[stepCount + 1, ]帧就单纯的discard掉片元单纯的保留Attachment让我们能够在RenderDoc里保存图片就可以了。

累加分子与分母

相除与discard

着了我将Blend设置为了 ,这样在累加阶段把srcAlpha设置成1就可以累加,在相除阶段将srcAlpha设置成分母的倒数就好了。

Blend设置

由于这部分在原理上涉及到波瓣之类的,所以单纯的半球采样需要换成与Roughness有关的重要性采样得到H,再由H得到需要采样的光线方向。

根据H计算L

但这里在实际采样的时候还用到了Mipmap,原理大概是概率低的样本采样需要大一些这样更平均一些,正好就采样更高层级的Mipmap,而概率高的样本采样小一点这样更精准,正好采样低层级的Mipmap,具体的说法可以看这里的20.4:

​通过上面这样一个公式计算出Mipmap登记后,直接采样就可以了。

​其他的就没什么了,保存的时候要拔全部的Layer和Mipmap都存成exr格式才行,所以json才会这么长:

最后也是狂采1048576次,但其实Roughness0的图直接用环境HDR图Blit过来就可以了,没有差别的,只是我懒得再改了,并且Mipmap层级高了以后样本数也可以减少一些的,因为本身需要的分辨率低了很多,不过我也懒得去改了。。。

生成LutMap

这部分就比较简单了。不用画6个面也不用画Mipmap,而且因为相当于就是在法向半球上做的预计算,所以直接在单位半球上计算就可以,转换矩阵也不需要了。

只需要累加就可以了

这里也需要根据Roughness生成H,单位半球所以Normal就是(0, 0, 1),根据输入的 也可以计算出View来,所以算就行了。分片和之前一样,Blend还是累加的设置。

采样计算并累加

而至于输入的参数 和Roughness,我是直接用gl_FragCoord除以Attachment的分辨率来获得的原点在左上角的uv坐标,分别作为 和Roughness就好了。

根据像素位置计算输入参数并反算View

这样就可以直接算出来了,不过考虑到R32G32_SFLOAT格式的Attachment并不是很好存储,所以我就又做了一次打包操作,先使用packHalf2x16将A、B两个float打包成一个uint,再将这个uint由高位到低位存储在一张R8G8B8A8_UINT的RGBA四个通道上,而R8G8B8A8_UINT可以直接存成png格式的图片,这就很方便了,只不过实时渲染使用的时候需要多一次解包的操作。

先采样再打包

​精度损失其实问题不大,毕竟是half,位数还是足够的,并且这个IBL的近似本身误差就大,完全不差这一点点。写一个解包的完全看不出来有区别。

source、pack、unpack

组合

已经把预计算的算完了,实时渲染只需要算一些当时才能确定的部分,照着公式组合起来就好了。

这里我也写成了宏定义的形式,最后再把这部分和之前的PBR结合起来就好了。

组合PBR和IBL

最后的效果也还行,不过可能是PrefilteredEnvironmentMap分辨率只有512*512的关系,反射强的部分模糊点,有锯齿。不过确实比之前最简单的Phong模型漂亮多了,真不错。

:||

总算是快写完了,这篇总结我都写了得有三四天了,虽然大多时候都在摸鱼。。。不过这部分确实比以前实现的部分麻烦很多,主要是理论部分很难彻底搞懂,我现在还有部分细节不是特别清楚,不过也算是差不多了。其实11月的时候还是很难过的,主要IrradianceMap部分最后的效果一直有点问题,低差异序列部分的bug一直没找出来,并且实验室的项目也是做不下去,完全不是代码上的问题,甲方配套的设备真的是有点烂,好在现在PBR+IBL这部分算是告一段落了,也不算是两手空空吧。

我一直再考虑要不要把这个Vulkan渲染器重写一遍,最近也在找一些好用的库了,准备变得nb一点,主要是现在想加一些需要预计算的东西很难加,而且这个框架已经是半年多前写的了,虽然一直修修补补的,但感觉也到了另起炉灶的时候了,上个月跑去学习了一下EffectiveC++,感觉也是挺nb的,之前自己写的都是一些比较朴素的设计方法,总之就是肯定会重写了。

之后应该会先继续在这个代码库做一些实验性的修改,只做验证用了,毕竟现在的Shader-Material-RenderPass-FrameBuffer简直是一坨狗食,之前说的合批、优化DescriptorSet也还没做,可能就会先试下这两个了。GI、SSR可能会在重写过后在新的代码库里做了,还想加一个简单的编辑器界面让它看起来更nb一点。

今年中旬也该投简历找工作了,幸好最近已经没有像去年暑假那样焦虑了,继续前进吧!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值