作者Renzokuken (狮子心)
看板GameDesign
标题[程式] Unheard Engine - 自制Vulkan小引擎
时间Thu Nov 24 03:46:04 2022
大家好, 不才名字Squall
目前在英国从事Graphics Programmer, 参与研发AAA专案
即使平常在工作上已经收获许多
我个人是那种不用别人催也会想办法变强的个性
所以平常是会开一些Side Project给自己练习的
这次的Vulkan小引擎 - Unheard Engine便是其中之一了
https://i.imgur.com/WygBvq1.jpg
利用空闲时间实作约57天, 目前的功能还没有很多
- Deferred Rendering Pass (PBR)
- Ray Tracing Shadow
- Lighting Pass
- Skybox Pass
- Motion Vector Pass
- Tone mapping
- Temporal AA
但从物件渲染、後制特效、光追的使用
应该涵盖各种基本的用法了
希望能帮到刚好也在学习Vulkan的人:)
然後也不用问为什麽没有半透明物件, 为什麽没有XXX之类的
就只是还没实作而已, 一切从0开始做是需要点时间的
任何没实作的功能都是Future Work, no ETA
网页好读版 (我发在GameDev.net的文章):
https://tinyurl.com/5t9824xu
由於篇幅有点过长, 我不会在这提所有的细节 (尤其是程式码)
那如果你是那种喜欢直接观察程式码的人
可以直接下拉到置底连结
● 引擎名称
由於过往使用过的引擎刚好都Un-开头 (Unity, Unreal)
我就随便抓个Un开头的字来用了XD
以下我会简单称为UHE
如果你对Vulkan程式有兴趣, 我建议还是从官方教学开始
● 环境
如同多数的学习者一样, 我从下列两个网站开始:
https://vulkan-tutorial.com/
https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html
所以你大概预期UHE是Vulkan+GLFW+GLM这种组合, 但,恰好相反!
UHE的组合为Vulkan + Win32 Window + DirectX math
+ HLSL + WIC texture loader + FBX importer
主要是因为我已经有DirectX 12的开发经验了
我想最大化利用学习时间, 尽量维持在我熟悉的环境
跨平台?以後想做的时候再改就行了
所以这边只有Vulkan / FBX SDK对我来说是陌生的
然後我是在笔电上开发的
i7 - 11375H, RTX 3060 Laptop, 16GB RAM
● Debug vs Release Build
我有特别区分这两种Build
一些功能像是Editor不会出现在Release Build
● Vulkan的误报错误
如果真的有把UHE载下来跑, 可能会遇到两种误报
1. VUID-VkSwapchainCreateInfoKHR-imageFormat-01778(ERROR / SPEC)
VUID-VkImageViewCreateInfo-usage-02275(ERROR / SPEC)
这个问题, 我是只有在开启RTSS的时候才遇到的
我猜其他FPS测量工具可能也会造成此问题
2. Microsoft C++ exception: MONZA::IgcThreadingContext::msg_end
在呼叫vkCreateSwapchainKHR()时有可能会存在的例外
以上这两种误报都不影响UHE的运行
如果有人对於此两种误报有解决经验, 还请不吝指点!
● 素材汇入
由於还没有实作一些编辑器, 目前UHE将会自动汇入RawAssets/下的素材
然後加入自制的Renderer架构里
素材汇入有着简单的快取机制, 汇入过的素材会生成.uhxxxx档案
下次再启动时, 已汇入过的素材不会再汇入(除非有变)
不然说真的汇入FBX可是很卡的..
● Shader编译
DirectXShaderCompiler才是真的MVP!
Vulkan的shader模组其实采用了他们的Spir-V语言
所以HLSL是需要转换成Spir-V的
只要在呼叫dxc.exe时传入 -spirv 参数就能转换
另外也为了光追shader传入了-fspv-target-env=vulkan1.1spirv1.4
UHE也有简单的shader变体管理, 同个shader但使用不同keyword的
我会给他们产生不同的hashcode
https://i.imgur.com/KoJyvp6.jpg
● Culling
目前完全没有做culling, 也还没有做draw call batching
另外要提的是为了最佳深度精度, 使用了reversed infinite z
所以物件离镜头再远都绝对不会被剔除
效能优化是之後才要做的事
目前Release Build大概是跑280~340 FPS在跑 (不开Vsync)
● Pool机制
UHE会尽量重复使用性质相同的物件
例如目前的维京小屋测试虽然有着747个Draw Calls
但实际上只用到了16个VkPipeline物件
以及只有5个VkSampler物件等等
相信我, 如果每个Renderer都生不一样的物件, 效能绝对降
● 渲染流程
目前很简单, 一个Main Thread + Render Thread
所有渲染工作都是在RT上完成
物件Constant Buffer或Storage Buffer的更新
采用了Dirty Flag机制, 也就是说大多数静态物件
根本不需要每个物件每个Frame都做Buffer Copy的动作!
流程和开头说得差不多:
https://i.imgur.com/HbVBFIG.jpg
1. Base Pass - Filling GBuffer
要点在於: 如何建立MRT (Multiple Render Target)
Vulkan教学网站只教了单一RT的使用
但其实很简单, 在建立vkFrameBuffer物件时
所使用的VkAttachmentDescription, VkAttachmentReference
改成复数就好了
2. 更新Top Level Acceleration结构
对於DXR光追不熟的人可以参考这个连结:
https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html
简单来说Acceleration Structure是光追必备的结构
分成Top Level, Bottom Level两种
前者是基於Instance的, 後者是基於Mesh的
可以简单把他们的关系想成Renderer跟Mesh
一个Mesh可以被不同的Renderer使用
一个Bottom Level AS可以被不同Instance Level AS使用
UHE给模型建立Bottom Level AS (无Transform)
然後给Instance建立Top Level AS (包含Transform)
当然这只是UHE的做法, 你要给Bottom Level AS施加Transform也是可以的
详细的建立过程请参阅程式码, 真的不好转过来
3. 光追阴影
https://i.imgur.com/yy7uYgv.jpg
https://i.imgur.com/Ggq38g9.jpg
多重阴影有考虑到, 取样时也有施加PCSS
所以距离遮挡物较近的影子比较锐利, 远的比较柔和
这边建立了一个Ray Generation Shader跟Closest Hit Shader
以及Any Hit Shader
并没有第一条试探Ray, 而是直接把深度转World Posion
直接从该位置进行光追就好了, 省掉试探Ray的损耗
重点在於, 如何获得击中物的材质/模型资讯?
由於Vulkan不像D3D12有Local Descriptors可以使用
我只好把资料都集中为Descriptor Array了:
Texture2D UHTextureTable[] : register(t0, space1);
SamplerState UHSamplerTable[] : register(t0, space2);
StructuredBuffer<VertexInput> UHVertexTable[] : register(t0, space3);
ByteAddressBuffer UHIndicesTable[] : register(t0, space4);
把Texture2D, StructuredBuffer再定义成Array!?
对, 这在D3D12, Vulkan都是做得到的
但一定要Shader Model 5.1以上
如此一来, 就能利用光追Shader的内建函式InstanceIndex()
或InstanceID()来取得正确资料了
在UHE, 我用InstanceID()来对应材质
用InstanceIndex()来对应模型
4. 光源Pass
结合GBuffer的灯光计算步骤
目前仅实作了Directional Light
没什麽好讲, 阴影取样, BRDF计算都在这
Indirect Lighting只简单使用了GroundColor + Normal.up * SkyColor
未来会考虑SH9
5. Skybox Pass
更没什麽好讲, 就是画Skybox
不过Vulkan初学者大概会关心: 怎麽建立TextureCube?
由於UHE并没有汇入dds, 一种可以直接存TextureCube的格式
(也是很旧的格式了)
所以必须用6张Texture2D来建立
也就是将Texture2D复制到TextureCube不同的Slice, 以及MipLevel
记得在建立Vulkan物件时使用VK_IMAGE_VIEW_TYPE_CUBE
以及VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT
然後Layer Count一定要是6这样
6. Motion Vector Pass
由於实作了TAA, 移动向量是一定要用来解决一些问题的
UHE分成两种Pass: Camera Motion, Object Motion
Camera Motion简单利用转换Depth Buffer -> World Position
并计算移动向量, 适合静态物件
Object Motion当然就是为了移动物件了
一样用了Dirty Flag机制, 不会移动的物件, 没有pass的必要!
7. Tone Mapping
采用了Stephen Hill (@self_shadow)的ACES
https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
简单来说这位老兄将ACES需要的转换统整成只要两个矩阵
相当的好用!
8. TAA
也是近代引擎的AA常客了, 但有几个问题要处理:
- 鬼影: 只要取得正确的History UV就行了
- Disocclusion: 前一个Frame被遮挡的物件, 在这个Frame突然出现
, 也一样会产生鬼影, 我采用了"motion rejection"来解决
若history motion与motion的差值大过一定门槛, 就不取样history
- Missing Depth: 像是skybox这种没有深度值的像素, 在算motion vector
时是会有问题的, 简单做了3x3检查, 范围内如果没有深度值, 也不取样history
如此一来, 鬼影问题便解决了
9. SwapChain Present
直接利用vkCmdBlitImage()来将後制後的结果present到swap chain
这个函式同时也会帮你做linear-sRGB转换
另外UHE的机制是分开了Swap Chain跟渲染解析度
例如视窗尺寸如果是1600x900, 还是可以渲染在1920x1080
为何要这麽做呢? 理由是为了全萤幕模式
我发现vkAcquireFullScreenExclusiveModeEXT()
并没有像IDXGISwapChain::SetFullscreenState()那样
呼叫了就进入全萤幕模式
这边必须自己resize视窗成桌面解析度
再使用vkAcquireFullScreenExclusiveModeEXT获得独占权
也就是说, 表现起来更像"无痕视窗"全萤幕
我不希望解析度被桌面解析度绑住呀!
● 总结
终於打完了, 如果你真的耐心地看到了最後, hold my beer!
个人几个对於Vulkan的想法
- 比D3D12更冗长的实作, 尤其是物件管理方面, 每个vk物件都有
对应的vkDestroyXXX()...
- Vulkan似乎没有Thread-Safe vkQueue, vkCommandBuffer也不能
执行後马上reset再重复利用, ID3D12CommandQueue则是Thread-Safe
ID3D12CommandList也能执行後马上再录制, 这种特性差异对平行化
设计有着重大影响
- Vulkan没有Local Descriptor的样子
- 整体来说, Vulkan几乎跟D3D12一样强大, 我猜它在Linux平台会更强
很高兴现在自己是D3D12/Vulkan双刀侠了!
The GitHub Link (code only):
https://github.com/EasyJellySniper/Unheard-Engine
The Full Project Link (including assets):
https://mega.nz/file/p0ICFJbJ#TL5Vdu6FEyCFdCwd_7rhT3N_UaLfb4HkG1XMP9cYMzA
我十分建议下载完整专案, 毕竟有一些素材
感谢收看!
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 80.47.253.201 (英国)
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/GameDesign/M.1669232776.A.469.html
※ 编辑: Renzokuken (80.47.253.201 英国), 11/24/2022 03:48:26
1F:推 iLeyaSin365: 真猛 有生之年我想我都不会做到开发引擎这种事 11/24 04:52
2F:推 dklassic: 赞欸感谢分享 XD 11/24 07:42
3F:→ dklassic: 感觉对 Unity 失望透顶的时候会想要来试试看(? 11/24 07:42
对Unity失望先试试UE吧! 自主制作的话
一堆"理所当然"的元件都得自己写
不过确实是挺好玩的:)
4F:推 luckyexcel: 强大 11/24 07:43
5F:推 zxc9764315: Vulken教学还躺在我的书签里面XD,感谢分享 11/24 07:56
6F:推 KanzakiHAria: Unreal表示:都在我的source code里 去找吧 11/24 11:38
7F:推 Mchord: Respect 11/24 12:00
8F:推 coolrobin: 请手下我的膝盖 m(_"_)m 11/24 13:02
9F:→ coolrobin: 收 11/24 13:03
10F:推 kyushu: 太强了,你跟另一个wicked engine的作者还满像了 11/24 17:37
wicked也不错, 有几次google到他的东西
11F:推 a82611141: 好强 11/24 18:55
12F:推 newyellow: 最近在学 webgl,对所有写底层的人感到敬佩 XD 11/24 20:34
GL team真的赞, 不畏强权微软
※ 编辑: Renzokuken (80.47.253.201 英国), 11/25/2022 02:38:09
13F:推 aegis123321: 感谢分享 网志有不少感兴趣的文章 11/25 07:17
14F:推 nicetw20xx: 好猛 推 11/25 08:21
15F:→ dklassic: 我已经对 Unity 的可靠度失望到开始自己写很多理所当然 11/25 15:02
16F:→ dklassic: 的东西了 XD 11/25 15:02
17F:→ dklassic: 但当然自制引擎就主要是顺便学习,如果目的是做商业游 11/25 15:02
18F:→ dklassic: 戏的话应该也是转 Godot 不是转 UE( 11/25 15:02
19F:推 NCTU87: 推 11/26 09:59
20F:推 johnny94: 推,自干过引擎,真的很累但很好玩 11/26 10:21
21F:推 EasyIsBeauty: 超猛,大学project跟同学一起干过後就懒了 XD 11/28 17:11
22F:推 RadiationXen: 推 12/03 08:46
23F:推 sck921: 感谢分享,开发引擎真的是佛心 12/07 12:20
24F:推 purplvampire: 太强了! 01/20 20:33