究极摸鱼挂科王终于击败了无敌可怕Vulkan大魔王
序
若是你所期望的,那定会得到强烈的回应。
——M八七
距离上次写知乎已经过去了三个月,一学期都过去了,还真是快。
写完上一篇文章后,我又继续完善了一下那个软渲染器,可以连续显示移动物体了,不过由于效率极低,复杂一点的一帧就要一两秒,并且基本上写完了,所以就一直没有更新那个了,转头去学vulkan了,最终就是做了个这么个东西,我愿称之为“带有对象管理和异步加载的接口像Unity的Vulkan渲染器”:
反射,多光源法线,折射,破碎玻璃,天空盒
对象遍历,包括视椎体剔除
资源池
Github地址是,200+个提交,着实累到我了:
FREEstriker/Air_Forward (github.com)github.com/FREEstriker/Air_Forwardhttps://link.zhihu.com/?target=https%3A//github.com/FREEstriker/Air_Forward
这是一个用Vulkan实现的Forward渲染器,基于c++17标准,使用了SpirvReflect、Glfw、FreeImage、Glm、Assimp、Rttr、NlohmannJson库,现阶段完成的特性包括:
vulkan对象管理
多线程绘制
视椎体剔除
Opaque Pass、Background Pass、Transparent Pass
多线程资源加载
模仿unity的对象管理
未完成的部分:
资源销毁
vulkan对象销毁
序列化反序列化
这三个月学vulkan着实学的我头大,也太复杂了,当初看到vulkan tutorial,画个简单图形居然需要快一千行,光理解那个tutorial就用了半个月,并且说的也不够清楚,只能再查手册、博客和stackoverflow,并且vulkan的资料是真的少,有问题只能去google上找,中文网站上全是如何配置vulkan,就离谱,一键安装的东西竟然有这么多教程,真的搞不懂。
至于为啥不学opengl去学vulkan,我只能说是我草率了,想着既然vulkan是新一代api那我肯定学新的啊,ε=(´ο`*)))唉,只能说早知道这么麻烦我肯定去学opengl。。。
因为分了三个线程:Graphic、IO和Logic,所以下面按着顺序说一下实现思路。
Graphic
麻烦的对象封装
显然,vulkan是非常繁琐的,如果之后要长时间的使用它,最好自己封装一个简单的框架,减轻之后的工作量,
我将它大概分成了:Asset、Command、Core、Instance。Asset主要包括一些需要从硬盘上加载的东西,例如网格着色器、贴图;Command包括一些绘制命令相关的类,例如CommandPool、CommandBuffer、Fence、Semaphore和Barrier;Core内包括一些初始化Vulkan用到的类,例如VulkanDevice、VulkanInstance和GlfwWindow;Instance内是一些对vulkan对象的简单封装,例如Image、Buffer、Memory等。
首先从初始化Vulkan说起:
我是采用了Creator的模式,Creator记录初始化配置,并进行硬件支持的检查,之后调用Create函数进行实际创建,但实际上初始化过程并不能完美的依次分到Window、Instance和Device三个类中,里面有一些需要其他类完成部分后才能创建的工作,所以我在里面加了一些隐含的对象初始化:
实线Create调用,虚线内部调用
之后是对Vulkan对象的一些简单封装,主要目的是减少在代码中大量的使用Vulkan的什么CreateInfo之类的东西,这些个东西写起来是真的太麻烦了,并且还加上一些RAII特性(不过并没有完全写完,只是一部分写了。。。)。
其中Image、Buffer、Sampler只是简单包装,而FrameBuffer、Memory、DescriptorSet都是使用对应的Manager类创建出来的,并且由Manager负责销毁,详细的会在下一个小节说。
Buffer的简单包装
Command空间内主要的部分是CommandPool和CommandBuffer。Buffer从Pool中创建,并用一个string标记Buffer名称,便于寻找和Debug。
绘制命令的包装
因为之前写过Unity的Urp,所以对它的CommandBuffer也了解过一点,就照着unity把Vulkan的一些vkCmd开头的绘制命令实现在了CommandBuffer类内,便于使用。
并且我在CommandBuffer内天减了一个fence,提交命令后可以直接使用WaitForFinish()方法来直接等待命令完成,也算是为了方便使用吧。
WaitForFinish()
Asset命名空间下的类都需要从硬盘加载,所以使用了线程池,但是线程池和资源管理的具体实现先不在这一个小节说,这里主要是如何与Vulkan对接的。
网格使用Mesh类加载,其中使用了Assimp库,自动三角形化、创建法线、切线,之后就和Vulkan Tutorial一样加载就可以了,不过使用了上面说过的我自己包装的Buffer和CommandBuffer,可以看到大幅度简化了代码,舒服了。最后好要加上计算一个OBB包围盒,以便于视椎体剔除。
加载网格
Texture分为了2D和Cube,这两个其实流程基本一样,都是先从硬盘加载再传入暂存Buffer再传入Image,只是其中的Image不太一样,并且加载也略微不同。传入其中需要使用一下ImageMemoryBarrier以正确转换图像的格式,由于加载队列不需要渲染,且由逻辑线程保证加载完成,所以直接PipelineStage参数直接写top和bottom就好了。
加载TextureCube
最困难的部分是Shader类,因为里面涉及了参数反射,我是使用了SpirvReflect这个头文件,并且将反射出来的资源转换为自己的几种资源类型(我叫它SlotType):Texture2D、UniformBuffer和TextureCube,以减少适配反射数据的共同工作量。此外,我是将Vulkan的PipelineLayout、Pipeline放在了Shader类里,因为我是把Shader理解为一个完整的渲染执行流程。
SlotType
并且我照着Unity的Shader的样子,用json写了一个.shader文件,用于指定各个阶段的实际spv代码路径和一些类似于RenderPass、CullMode、BlendMode之类的参数,反正就是照着Unity抄。
.shader文件(由于Vulkan的参数类型其实都是uint32_t所以序列化后就是数字了。。。)
我将Shader加载过程分成了上面图中的几个函数:ParseShaderData加载并反序列化.shader文件、LoadSpirvs从硬盘读取spirv文件、CreateShaderModules创建Vulkan的ShaderModule和用于参数序列化的数据、PopulateShaderStage装填VkPipelineShaderStageCreateInfo数据、PopulateVertexInputState根据顶点着色器的in参数解析需要的顶点数据并装填对应Vulkan数据结构、CheckAttachmentOutputState检查像素着色器的输出是否和RenderPass的颜色附着相符合、PopulatePipelineSettings根据.shader文件里的参数装填pipeline所需的数据、CreateDescriptorLayouts和PopulateDescriptorLayouts将shader内绑定的数据组合转换为SlotType并创建DescriptorSetLayout、CreatePipeline进行实际的创建VulkanPipeline、最后使用DestroyData销毁暂存的数据。
可以看到真的挺麻烦的,当时Shader类也确实写了好长时间,Shader和Material可也说是最麻烦的两个类了,写的也是非常乱。
那些Manager们
先说MemoryManager吧,由于显存和内存不太一样,显存是有一个分配数量限制的,分配超过一定次数后就不能再次分配了,所以显存管理是有必要的,我是直接写了个最简单的。
首先对显卡支持的每种显存类型分配一个ChunkSet,在每次请求内存时,都会在所需要的显存类型的ChunkSet中寻找可用的显存;如果一个ChunkSet中没有Chunk,就会使用Vulkan的api请求一块大的显存,总之就是在ChunkSet寻找可用的Chunk;显存最小的单位是Block,请求时根据请求大小和字节对齐从可用的Chunk中取出一小块作为Block,并记录。Chunk是Manager向Vulkan请求分配的单位,Block是程序向Manager请求分配的单位,Chunk内分配Block使用首次适应策略,所以说是最简单的。
实线Chunk,虚线Block
对应VRAM类型和ChunkSet使用的是vector,因为VkMemoryRequirements内memoryTypeBits是一个uint32_t,所以把它隐含在vector索引中就可以了;ChunkSet存储Chunk使用的是map,VkDeviceMemory作为键,Chunk作为值,这样便于销毁时快速找到对应Chunk;Chunk内存储Block使用了两个map,一个作为分配空间表,一个作为未分配空间表,都使用起始相对字节地址作为键。
写这玩意的时候我就在想本科时候那个操作系统实验班做的miniOS,本来想改内存管理,不过作者写的太好根本无从下手,最后直接把注释从日文改成中文就交了,当时我还和大哥、sc在敦煌,哈哈哈哈,刚旅游完回家就开始疫情了。。。怀念没有疫情的日子。
FrameBufferManager的主要作用是管理FrameBuffer,我也将它分为了两个阶段,第一个阶段是添加Attachment,通过给Attachment给予名称的方式可以方便地获取到对应的Attachment,也方便Debug查看。
创建并获得Attachment
通过名称创建并获得Frameuffer
DescriptorSetManager中也使用了池,对每个SlotType创建一个DescriptorSet池,池中包括若干个Chunk,每个Chunk包含若干个一次性创建的VkDescriptorSet,这样就可以减少分配次数和实现描述符复用。至于为啥这个叫pool而MemoryManager里的叫ChunkSet是因为这两个不是一起写的,中间隔了一两周,都忘了。。。
而由于这个每个chunk里的东西都一样,所以使用pool使用map存储多个Chunk比较好,使用可分配描述符数作为键,可以加快分配时的速度。
LightManager主要用于将逻辑线程送来的灯光组件分为Main、Important、UnImportant、Skybox几种,并且选择后将部分数据写入设备Buffer中,功能非常简单,我并没有像Unity那样会选择效果强的作为重要光源,我是直接从灯光组件中选了前几个作为重要光,在选几个作为非重要光,其他的直接舍弃,差不多得了先有个样子就行。
筛选的灯光数据
RenderPassManager是比较重要的,它存储了一些RenderPass及其执行优先度,便于以正确顺序绘制图像,具体的实现下一节再说。
三个RenderPass
原来用Unity的时候,好像material下面会有一个Queue的选项,可以调节渲染顺序,好像shader里也有一个tag不过我记不太清了,urp就更是直接有RenderPass类了,反正就是根据记忆中的用法造了一个出来,简化版的,只根据RenderPass的渲染顺序数来确定渲染顺序。
我是将它分成了这么几个虚方法:
OnCreate会在将这个RenderPass加入到Manager时调用,用于配置一下创建的RenderPass,也是使用了Creator。
OnPrepare阶段会在OnCreate后调用,用于创建RenderPass所需要的FrameBuffer。
OnPopulateCommandBuffer用来填充CommandBuffer,这个方法是在线程池中执行的,所以要先从对应线程的CommandPool中分配一个CommandBuffer在进行填充,并且这个阶段只填充,不提交,因为填充结束顺序是不确定的,需要等待填充完毕后在进行提交。
OnRender阶段会进行实际的提交,并且需要配置一下Semaphore的依赖。
OnClear会在每帧绘制完毕后执行,用于销毁当前帧申请的CommandBuffer。
反正这几个我用的是挺好的,可以完成按顺序渲染的任务。我现在只实现了三个基本的RenderPass:不透明、背景和半透明。
其实这三个RenderPass没啥可说的,就很基本,需要注意的是半透明Pass,我是采用的通过Barrier限制向ColorAttachment写入的顺序,先执行远处的物体,在执行近处的,由于这个Barrier与加载时用的Barrier不同,它是在pipeline内部的,所以要在创建RenderPass时添加一个自我依赖,才能够正确的按序执行。
添加自依赖,注意blend阶段属于output阶段的read访问
draw后添加等待colorAttachment写入完毕的Barrier
不透明Pass就不用添加Barrier了
线程池
c++标准库并没有线程池、任务之类的东西,所以还是得自己搞一个。我是从GitHub上找的一个,然后自己又改成了需要的样子,通过向一个共有的任务队列中添加任务并获得一个future来满足延时获取的需求。
同时给每一个子线程一个CommandPool,用来满足多线程填充CommandBuffer的需求。
Shader中的计算
shader我是写的glsl,并使用glslangValidator编译为spirv,shader里启用了GL_GOOGLE_include_directive拓展,将一些可重用的方法都写在了单独的文件里。
环境光我是直接使用的从Skybox中采样的,虽然是不对的,但是将就用吧。
环境光
点光源和方向光的漫反射和高光反射都和之前写的软渲染器的算法一样,写在了Light.glsl中,没啥可说的。
Common.glsl中的函数也都很常规,比如坐标转换、方向转换、TBN之类的,也很常规。
Camera.glsl中有一个比较麻烦的函数PositionScreenToNearFlatWorld,也就是将屏幕坐标转换为世界坐标,由于之前没有关心过Vulkan的手相,导致怎么计算都不太对,就还是挺无语的。
其实原理很简单,就是坐标转换后根据相机的长宽数据、坐标数据和朝向数据还原出世界坐标,仔细想一下没有难度,搞清楚手相就好了。
有的时候还需要获得相机的视线,我是写了两种,一种是直接观察点-相机坐标,还有一种是根据相机类型来的,正交相机就是相机的前方向,透视相机是观察点-相机坐标,可以根据需要选择调用。
由于都写成了通用的方法,所以shader里是比较简洁的,哈哈哈哈。
此外,我还把非重要光的光照写成了顶点光照,就像上面一样。
反射没啥难的,折射需要注意球体有两次折射,不要忘了。
球体折射
显示流程
流程伪代码(隐藏了同步)
逻辑线程和图形线程是一个串行的流程,逻辑线程结束后才能进行图形线程,首先拷贝数据,接着对每一个渲染器做视椎体剔除,并把它放入对应的RenderPass的列表中,然后遍历RenderPass填充CommandBuffer,填充结束后提交命令,最后将渲染好的图像拷贝到交换链上并通知逻辑线程可以继续运行。
需要注意的是,由于坐标的问题,如果直接显示的话,它是上下颠倒的,网上有两种方法,一种是加一个拓展然后把视口的高度取个负值,另一种是给glsl的glPosition的y加个负号,对我来说两种都有点麻烦,我是直接在最后的拷贝时直接反着拷贝了(其实是Blit),因为我渲染时都当它是正的,所以效果也一样。
IO
IO内主要就是一个线程池和一个AssetManager,就是用来在逻辑线程内实现异步加载网格贴图之类的东西,我写的也是比较简单,线程池就和上面Graphic里的线程池一样,不过AssetManager和之前软渲染器里的不太一样,之前那个是每次用的时候都用一个key去Manager里找,这次改成了加载时找一次,之后只需要转类型就可以使用。
我是将资源分成了Asset和AssetInstance,每个路径的资源在AssetManager只会作为AssetInstance加载一次,加载后获得的是作为AssetInstance包装的Asset,Asset里会包含一个AssetInstance指针,参数都从AssetInstance中获取,没有修改资源的功能。
但实际上,这个写的还是有问题的,只是考虑到了加载时的同步,没有考虑加载卸载同时发生的情况,应该是后会再改一版。
Logic
对象管理
对象管理是从之前的软渲染器里搬过来再改的,GameObject的管理几乎一样,仍然是孩子兄弟二叉树,只是Component的管理又重新想了一下,完全改了。
首先要明确遍历Component的顺序,它应该每次只完全遍历一种Component,且顺序应该是先按照GameObject的层次顺序来,同一个GameObject内的Component应该按照添加顺序来,画个图就是这样:
先按GameObject层次遍历
GameObject内的多个Component按添加顺序
让我们先假设我们已经完成了GameObject的遍历,现在要对单个GameObject内的Component进行遍历,显然,如果我们把全部的Component都放在一个按添加顺序组织的数组里是不合适的,因为我们要一次性遍历同一种Component,不分类放在一起显然会极大地增加每次遍历寻找对应Component类型的时间。所以我们需要一种既能够分类又能够保留添加顺序的数据结构,我才用的是一种类似于十字链表的存储方式:
横轴连接同一类型的Component,纵轴连接全局添加顺序的所有Component,需要注意的是,横轴还包含了局部的添加顺序,添加一个Component时直接添加在对应类别横轴的尾部,并且也添加到纵轴的尾就可以了。
而关于横轴的多个Head的组织方式,我是采用了简化类型为枚举并使用map存储的方式:
反正遍历的时候按横轴就行了。
但实际上,GameObject的层次遍历也是一个问题,虽然孩子兄弟树是很好层次遍历的,但是需要考虑到Behaviour是可能会在遍历过程中修改对象树的,这就很麻烦了,可能深搜到一半本身早就被销毁了。因为之前在网上曾经看到过Unity是延迟销毁的言论,所以还思考了一下怎么个延迟销毁,其实我原来写的那个软渲染器就是延迟销毁的,无非就是先把它放到一个销毁池里,边遍历边打标签,遍历完在统一销毁,但其实涉及到的隐藏问题挺多的。
我实际选择的是遍历的时候把每个对象加到一个哈希集中,每遍历到下一个时就检查这个是否存在在哈希集中,如果不在就说明之前销毁掉了,跳过就可以,如果在哈希表中存在,说明这个对象是可以被迭代的。并且,不能简单的使用深搜的方法,因为GameObject可能会移动,所以必须遍历前就把它的孩子全部取出来,即使孩子们换了位置,也会在当前轮次中迭代到。
非常多的检测是否存在于哈希集中的代码
Component的反射
大家肯定都用过Unity里面的GetComponent()函数,这个还是挺好用的,可以直接通过类型或类型名来获得当前GameObject下的Component,由于C#是有反射的,而C++虽然有但是不是很完整,所以要想实现这个东西我是引入了一个Rttr的反射库,只要利用这个库,再结合自己的系统结构封装就可以了。
上面说道,现在的Component的十字链表的横轴的key是一个枚举类型,而要通过类型名来寻找,就肯定要先确定这个类型名属于哪一个枚举类型。我是直接用各个类型的基类和枚举值做了一个表,如果这个类是表中类的子类,那么就用对应的枚举值去Component十字链表的横轴中去找就可以了。
定位横轴后,需要在一堆同基类的Component中寻找符合要求的那一个Component,我用的逻辑是下面这样的:
图画烂了,凑活看吧
之所以当输入是子类时需要额外检查的原因是:虽然输入是子类但是有可能实际里面是new的一个基类,基类对象是不可能凭空变成子类的,所以要额外检查一下,这样就可以找到符合的Component了。
这个接口终于接近Unity了,真好。“这引擎我就不用了,怕认错引擎。”
生命周期
我是直接把生命周期虚方法写到了Component里,所有的Component都有生命周期,无非就是有的回调里面少有的回调里面多罢了(只有Behaviour回调里面多),函数名也是抄的Unity,哈哈哈哈:
不过现在由于序列化GameObject并没有做,所以OnAwake根本没有调用,OnDestroy的调用存在内存泄漏,只有OnStart和OnUpdate是正常的,下一个版本应该就会实现完整。
DEMO展示
做的展示场景里面有三个球,一个纯反射一个纯折射一个有贴图和法线,发现球还在不断自转;六个灯光,一个方向光,一个天空盒作为漫反射光源,还有四个球按照正四面体的顶点排列围绕法线球旋转;五个玻璃平面,是使用的半透明Pass渲染的;一个背景CubeMap,用来绘制背景;一个相机围绕着原点做旋转。
具体情况把代码跑一下就可以看明白了。反正就是这么个情况。
:||
干了三个月就做了这么个东西出来,而且也不太完整,只是能看罢了,里面隐藏的内存泄漏我想都不敢想,之后还得好好检查测试。
不过这个前向渲染我是应该不会更新了,我下面准备加上Tile Based Forward和Cluster Based Forward,框架的Debug应该会在那个里面做了,而且会添加一些新的东西,ComputeShader啥的,流程可能也会微调。现在研一结束了,要负责项目了,可能就只能闲的时候做了,不能像这个学期一样啥都不干就写这个了,可恶。
总之,写的时候困难重重,写后在回顾其实难度也不是那么的大,只是稍微繁琐一些,自己也是第一次一次性写这么多C++,只能说希望对以后找工作有帮助吧。
一会就要开组会了,其实我啥都没干,寄。
开完会去聚餐,为了减肥吃这一顿我从昨天午饭后就一口东西都没吃了,饿死我了。
这次原神的2.7版本,为了抽夜兰大姐姐,把我的原石全都抽完了,还歪了刻晴,不过我还挺想要刻师傅的,只是不知道下个版本的万叶抽不抽的出来了,唉,寄!
因为原神太长草了,所以我又下了个崩坏三,真实有年代感啊,不过手感还可以,哈哈哈哈。
还有,我需要看新奥特曼,呜呜呜。
究极摸鱼挂科王终于击败了无敌可怕Vulkan大魔王相关推荐
- 专科大学1c语言挂科,最容易挂科的7门课,你中枪了吗?
2021年高职单招升学一对一咨询高职单招刘老师:dxs18583993958(微信) 据说每到期末考试前,据说很多同学都会开始拜考神,保佑自己不要挂科,今天就来说说哪个科目容易挂科,看你有没有中枪! ...
- 白嫖我常用的 11 个超火的前端必备在线工具,终于有时间上班摸鱼了
大家好,我是你们的 猫哥,一个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ 前言 猫哥是一个常年混迹在 GitHub 上的猫星人,所以发现了不少好的前端开源项目,在此分享给大家. 公众号:前端GitHub,专 ...
- jdbc原生调用存储过程-------摸鱼王的日常问题
日常问题-jdbc原生调用存储过程 大家好,我是摸鱼王 今天上班老板让写个根据条件清理数据库数据的接口 然后我发现java代码执行起来有点慢 因为数据量特别大 于是 写了个删除的存储过程 很简单就不展 ...
- 写在最开始的话-------------我是摸鱼王
在工作(摸鱼)之余 看看有哪些视频----这不是广告 工作到今天也有三年多了 从最开始的勤勤恳恳工作到今天的摸鱼王 觉得写代码已经没什么难度了 然而作为一个老员工有时候会觉得公司新来的那些一看就是培训 ...
- 赶紧Mark!再也不怕领导偷偷出现在身后了,你才是最强摸鱼王
当你在上班摸鱼的时候,领导总会偷偷摸摸地出现在你的背后,例如小编曾经偷偷摸摸看<轻音>被抓包了.今天我们就用 Python 来破解这个摸鱼被抓的套路,主要的思路是用 opencv 调用电脑 ...
- c语言课程设计挂科率高吗,有没有挂科的人指教下怎么让老师把成绩改高呢???...
差3分就能及格了 晕死啊不想补考重修啊!!!! 有没有成功挽回老师分数的朋友给点招啊谢谢!!!! 人打赏 0人 点赞 主帖获得的天涯分:0 来自 天涯社区客户端 | 举报 | 楼主 | 楼主发言:35 ...
- 百度之星2019决赛摸鱼记
前言 绝对不鸽 没有的事 我云岛主已经在路上了 Day0 一个人来到了北京 真的很冷. 举目无亲,然后认识了dcx大爷,大爷非常平易近人体验极佳 然后就在酒店快乐聊天睡觉,一觉睡到了欢迎晚宴.嗯.北京 ...
- 互联网摸鱼日报(2023-03-16)
互联网摸鱼日报(2023-03-16) InfoQ 热门话题 3分钟上手,2小时起飞!教你玩转OceanBase Cloud! Facebook iOS版:探索移动应用10年演进之路 Netflix ...
- 互联网摸鱼日报(2023-01-13)
互联网摸鱼日报(2023-01-13) InfoQ 热门话题 开源大赛形式创新,参赛项目百花齐放 展望数据库工程师的 2023 |InfoQ<极客有约> 直击指标分析与管理痛点,一文详解K ...
最新文章
- 事件冒泡和阻止事件冒泡
- ik查看分词器:request body or source parameter is required/ missing authentication credentials for REST
- html获取qq头像代码,jQuery在线获取QQ名称和头像
- 利用ICSharpCore搭建基于.NET Core的机器学习和深度学习的本地开发环境
- 郫都区计算机学校,成都郫县好升学的计算机学校有哪些
- java jsp常见问题_jsp和servlet常见问题总结
- Puppet常用配置与管理
- 关于iBatis中的错误提示(必须以 或 /结尾,有时并不是你的结尾没有以 /结束,而是这个标签里面有问题!!)(更重要的是sqlMap的修改手段!!!)
- ERP计划参数如何在线更新
- poj 2754 Similarity of necklaces 2
- Vue:vue中使用layUI
- ubuntu 终端透明
- rog主板php,华硕主板有哪些系列 华硕主板各系列区别对比
- 基于深度学习的银行卡号识别 卡号识别和分割
- python图像质量评价_图像质量评价和视频质量评价(IQA/VQA)
- 音频播放AVAudioPlayer
- AIC,AIB,同德显卡五兄弟,
- 把汉字转换成拼音的util
- 微信红包又创新纪录 跨年夜发红包数达23.1亿次
- 坐在办公室里的人注意一下-喝水--鼠标手---脖子(颈椎)---腰部