diff --git a/.cache/plugin/git-committers/page-authors.json b/.cache/plugin/git-committers/page-authors.json new file mode 100644 index 00000000..ea9b7504 --- /dev/null +++ b/.cache/plugin/git-committers/page-authors.json @@ -0,0 +1 @@ +{"cache_date": "2023-12-01", "page_authors": {}} \ No newline at end of file diff --git a/.github/workflows/mkdocs_site.yml b/.github/workflows/mkdocs_site.yml index 2942f9cf..03fccb70 100644 --- a/.github/workflows/mkdocs_site.yml +++ b/.github/workflows/mkdocs_site.yml @@ -10,7 +10,10 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4.1.1 + with: { fetch-depth: 0 } + - uses: actions/setup-python@v4 with: python-version: 3.x diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..7c7eb74e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,77 @@ +cmake_minimum_required(VERSION 3.12) + +project(ModernGraphicsEngineGuide CXX) + +set(INSTALLATION_PACKAGE_OUTPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/InstallationPackage) +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP "Generated Files") +set(CMAKE_MAP_IMPORTED_CONFIG_DEBUGEDITOR Debug Release) +find_package(Qt6 COMPONENTS Core Widgets Gui Multimedia ShaderTools REQUIRED) + +function(add_shader TARGET_NAME SHADER_PATH) + set(OUTPUT_SHADER_PATH ${SHADER_PATH}.qsb) #输出文件路径 + add_custom_command( + OUTPUT ${OUTPUT_SHADER_PATH} #指定输出文件 + COMMAND qsb.exe -c ${SHADER_PATH} -o ${OUTPUT_SHADER_PATH} --glsl 430 --msl 12 --hlsl 60 #执行QSB工具 + MAIN_DEPENDENCY ${SHADER_PATH} #指定依赖文件,即该文件变动时,触发上述命令 + ) + set_property(TARGET ${TARGET_NAME} APPEND PROPERTY SOURCES ${OUTPUT_SHADER_PATH}) #需要把输出文件添加到一个构建目标中,才会触发CustomCommand + source_group("Shader Files" FILES ${SHADER_PATH} ${OUTPUT_SHADER_PATH}) #将着色器文件分类 +endfunction() + +function(add_example EXAMPLE_PATH) + get_filename_component(EXAMPLE_NAME ${EXAMPLE_PATH} NAME) + file(GLOB_RECURSE PROJECT_SOURCE FILES ${EXAMPLE_PATH}/*.h ${EXAMPLE_PATH}/*.cpp ${EXAMPLE_PATH}/*.qrc) + add_executable(${EXAMPLE_NAME} + ${PROJECT_SOURCE} + ) + target_link_libraries(${EXAMPLE_NAME} PRIVATE QEngineLaunch) + qengine_copy_dll(${EXAMPLE_NAME}) +endfunction() + +function(add_example_dir DIR_PATH GROUP_NAME) + file(GLOB EXAMPLE_LIST RELATIVE ${DIR_PATH} ${DIR_PATH}/*) + foreach(EXAMPLE_NAME ${EXAMPLE_LIST}) + if(NOT EXISTS "${DIR_PATH}/${EXAMPLE_NAME}/Source") + add_example_dir(${DIR_PATH}/${EXAMPLE_NAME} ${GROUP_NAME}/${EXAMPLE_NAME}) + elseif(IS_DIRECTORY ${DIR_PATH}/${EXAMPLE_NAME}) + add_example(${DIR_PATH}/${EXAMPLE_NAME}) + set_target_properties(${EXAMPLE_NAME} PROPERTIES FOLDER ${GROUP_NAME}) + install(TARGETS ${EXAMPLE_NAME} DESTINATION ${INSTALLATION_PACKAGE_OUTPUT_DIR}) + endif() + endforeach() +endfunction() + + +add_subdirectory(Source/0-QEngineUtilities) + +add_example_dir(${CMAKE_CURRENT_SOURCE_DIR}/Source/1-GraphicsAPI 1-GraphicsAPI) +add_example_dir(${CMAKE_CURRENT_SOURCE_DIR}/Source/2-EngineTechnology 2-EngineTechnology) +add_example_dir(${CMAKE_CURRENT_SOURCE_DIR}/Source/3-GraphicsTechnology 3-GraphicsTechnology) + +# 部分示例的特殊操作 +add_shader(03-Shader ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Shader/color.frag) +set_property(TARGET 00-Blur PROPERTY AUTOMOC ON) +set_property(TARGET 01-Bloom PROPERTY AUTOMOC ON) +set_property(TARGET 02-Outlining PROPERTY AUTOMOC ON) +set_property(TARGET 03-SSAO PROPERTY AUTOMOC ON) +set_property(TARGET 04-DepthOfField PROPERTY AUTOMOC ON) +set_property(TARGET 00-RenderingArchitecture PROPERTY AUTOMOC ON) +set_property(TARGET 05-GPUParticles PROPERTY AUTOMOC ON) +set_property(TARGET 03-SSAO PROPERTY AUTOMOC ON) + +execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/Resources ${CMAKE_CURRENT_BINARY_DIR}/Resources) + +get_property(QENGINE_TARGET_LIST GLOBAL PROPERTY QENGINE_TARGET_LIST) +foreach(QENGINE_TARGET ${QENGINE_TARGET_LIST}) + get_target_property(PLUGIN_TYPE ${QENGINE_TARGET} PLUGIN_TYPE) + if(PLUGIN_TYPE) + install(TARGETS ${QENGINE_TARGET} DESTINATION ${INSTALLATION_PACKAGE_OUTPUT_DIR}/Plugins) + else() + install(TARGETS ${QENGINE_TARGET} DESTINATION ${INSTALLATION_PACKAGE_OUTPUT_DIR}) + endif() +endforeach() + +install(DIRECTORY Resources DESTINATION ${INSTALLATION_PACKAGE_OUTPUT_DIR}) + +install(CODE "execute_process(COMMAND windeployqt 00-Blur.exe WORKING_DIRECTORY ${INSTALLATION_PACKAGE_OUTPUT_DIR})") diff --git "a/Docs/00-C++/1.\350\256\241\347\256\227\346\234\272\345\217\221\345\261\225\345\217\262.md" "b/Docs/00-C++/1.\350\256\241\347\256\227\346\234\272\345\217\221\345\261\225\345\217\262.md" index bfd5386c..f5ae808f 100644 --- "a/Docs/00-C++/1.\350\256\241\347\256\227\346\234\272\345\217\221\345\261\225\345\217\262.md" +++ "b/Docs/00-C++/1.\350\256\241\347\256\227\346\234\272\345\217\221\345\261\225\345\217\262.md" @@ -27,7 +27,7 @@ comments: true 现在让我们来想想一根绳子有多大的潜力: - 可以打结来记录今天星期几。 -- 可以留下一根绳子在家里,打结或不打结,家人回到家一看绳子就知道自Q己晚上回不回家吃饭。 +- 可以留下一根绳子在家里,打结或不打结,家人回到家一看绳子就知道自己晚上回不回家吃饭。 - ... 总结一下就是,绳结可以用来计数,也可以根据数字, **解读** 出 **相应的** 逻辑,这也就意味着,任何事物,都可以利用绳结进行记录,但这有个前提—— **绳子够长** diff --git "a/Docs/01-GraphicsAPI/0.\346\246\202\350\277\260.md" "b/Docs/01-GraphicsAPI/0.\346\246\202\350\277\260.md" index 8c8df64d..ce0fce92 100644 --- "a/Docs/01-GraphicsAPI/0.\346\246\202\350\277\260.md" +++ "b/Docs/01-GraphicsAPI/0.\346\246\202\350\277\260.md" @@ -1,3 +1,7 @@ +--- +comments: true +--- + # 图形API概览 在开始学习之前,你需要确切地意识到 C/C++ 只是一个将 **现实理论** 在 **计算机中** **变现** 的 **工具** ,并且能够熟练使用它,否则,你应该继续潜修,在没构建好完备的基础知识体系和良好的代码素养之前,笔者认为是没有能力甚至没有资格去进一步学习的。 @@ -101,7 +105,7 @@ void drawLine(std::vector>& frameBuffer, Line line) { drawPoint(frameBuffer, line.points[0]); } - float steps = abs(dx) > abs(dy) ? abs(dx) : abs(dy); + int steps = abs(dx) > abs(dy) ? abs(dx) : abs(dy); float xInc = dx / (float)steps; //x增量 float yInc = dy / (float)steps; //y增量 @@ -151,7 +155,7 @@ void renderFrame(std::vector>& frameBuffer) { 思路细节请阅读: -- http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.htm +- http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html 这里有一个简单的实现: @@ -520,7 +524,7 @@ int main() { 在这个问题上,每个人都有自己的见解,比如: -- [知乎:Vulkan相比于OpenGL、DX12、Metal和Mantle有什么优势、劣势?]() +- [知乎:Vulkan相比于OpenGL、DX12、Metal和Mantle有什么优势、劣势?](https://www.zhihu.com/question/46536915) 作为一个过来人,笔者的看法是: @@ -743,32 +747,6 @@ include(QtTestHelpers) ![ct7](Resources/ct7.gif) -### QEngineUtilities - -[QEngineUtilities](https://github.com/Italink/QEngineUtilities/tree/e63bbb78630848b5033ba13ee6ca12258b41df1c) 是笔者一直在迭代的渲染工具库,它包含三个Target: - -- **QEngineCore** :渲染架构,包含RHI、FrameGraph、RenderPass、RenderComponent、Asset的简易封装。 -- **QEngineEditor** :编辑器套件,包含一些基础属性调整控件,以及基于QtMoc的DetailView。 -- **QEngineUtilities** :Lanuch层,对上面两个模块进行组装,例如在`DebugEditor`配置下,会嵌入编辑器,而在`Debug`配置下,就只有Core模块。 - -它主要用于教学和尝试: - -- 强调可读性是第一要素 -- 没有细致地追求性能(代码细节上有一些瑕疵,在笔者察觉到的时候已经太晚了,由于精力有限,目前笔者也只能选择妥协,非常抱歉...不过放心,这些影响微乎其微) -- 以渲染为核心,包含少量编辑器架构,不会引入一些会导致代码臃肿的模块,如资产管理,网络,异步,ECS... - -一个简单的使用示例如下: - -![image-20230225112034379](Resources/image-20230225112034379.png) - -![image-20230225111719777](Resources/image-20230225111719777.png) - -在接下来的教程中,将会在这个模块上一点一点的累积入门的基础知识,目前Github仓库位于: - -- https://github.com/Italink/ModernGraphicsEngineGuide/tree/main/Source - -> 非常抱歉目前还有一些遗留的小问题,它们会在后续的章节中修复,真的太肝了~ - ## 学习资源 该教程的主要目的是为了让一些小伙伴能够入门,并培养良好的代码习惯。 diff --git "a/Docs/01-GraphicsAPI/1.\345\237\272\347\241\200.md" "b/Docs/01-GraphicsAPI/1.\345\237\272\347\241\200.md" new file mode 100644 index 00000000..c03a3e28 --- /dev/null +++ "b/Docs/01-GraphicsAPI/1.\345\237\272\347\241\200.md" @@ -0,0 +1,564 @@ +--- +comments: true +--- + +# 图形API基础 + +## 基础概念 + +现代图形API对于初学者而言,一开始存在很多莫名其妙的结构,不用过度担心,这些名词只不过是给一些特定结构和流程一个能够称呼的简短昵称,当你了解它的应用场景和熟悉它的使用流程,自然会明白它们的意义。 + +### 物理设备(Physical Device) + +在图形API中,我们一般称 GPU 为 **物理设备** + +随着GPU的快速发展,图形API的功能特性也在与日俱增,为了让图形API的使用能够向下兼容GPU设备,图形API往往会提供一些接口来查询物理设备是否支持某些特性以及资源的限制。 + +以Vulkan为例,使用 **VkPhysicalDeviceFeatures** 可以查询物理设备是否支持某种特性: + +```c++ +struct VkPhysicalDeviceFeatures { + VkBool32 robustBufferAccess; + VkBool32 fullDrawIndexUint32; + VkBool32 imageCubeArray; + VkBool32 independentBlend; + VkBool32 geometryShader; + VkBool32 tessellationShader; + VkBool32 sampleRateShading; + VkBool32 dualSrcBlend; + VkBool32 logicOp; + VkBool32 multiDrawIndirect; + VkBool32 drawIndirectFirstInstance; + VkBool32 depthClamp; + VkBool32 depthBiasClamp; + VkBool32 fillModeNonSolid; + VkBool32 depthBounds; + VkBool32 wideLines; + //... +} +``` + +使用 **VkPhysicalDeviceLimits** 可以查询物理设备的资源限制: + +``` c++ +struct VkPhysicalDeviceLimits { + uint32_t maxImageDimension1D; + uint32_t maxImageDimension2D; + uint32_t maxImageDimension3D; + uint32_t maxImageDimensionCube; + uint32_t maxImageArrayLayers; + uint32_t maxTexelBufferElements; + uint32_t maxUniformBufferRange; + uint32_t maxStorageBufferRange; + uint32_t maxPushConstantsSize; + uint32_t maxMemoryAllocationCount; + //... +} +``` + +开发者在使用图形API时,需要知道这些限制的存在,并且在使用时除了找出符合自己功能要求的物理设备,还需要在开发时为某些设备特性不支持的用户做一些逻辑上的兼容性处理。 + +### 逻辑设备(Device) + +逻辑设备是开发者与物理设备沟通的 **“代理商”** + +因为CPU和GPU是各自执行互不等待的,为了减轻它们之间通信的负担,让开发者不要因为一点点鸡毛蒜皮的事情就追着GPU大喊:“好家伙,这个活你不给我整完你还想跑?” + +现代图形API中就提供了 逻辑设备 这一个概念,让开发者有什么事情跟代理商去说,代理商攒够一堆事情之后,再统一去告诉物理设备去执行。 + +> 在创建逻辑设备时,需要指定开启物理设备的哪些扩展。 + +### 指令缓冲(Command Buffer) + +指令缓冲也叫 Command List ,它是 开发者 的 **"小纸条"** + +开发者除了把一些货物交给 代理商(逻辑设备),还会把一些需要做的事情都记录到一张小纸条上,交给 代理商 转达给 物理设备。 + +> 在QRhi中,不能直接创建CommandBuffer,它的途径有两种: +> +> - 通过 SwapChain 可以拿到当前的 CommandBuffer +> - `rhi->beginOffscreenFrame` 会创建一个新的 CommandBuffer +> +> **QRhiCommandBuffer** 支持如下指令: +> +> ![image-20230305213442549](Resources/image-20230305213442549.png) + +### 设备队列(Device Queue) + +设备队列是 代理商(逻辑设备)和 物理设备 沟通的唯一 **”桥梁“** + +当 代理商 拿到 开发者 送来的 货物 和 小纸条,会快马加鞭地跨越桥梁, 赶向 物理设备 + +### 同步(Synchronization) + +GPU 为什么这么强,是因为它手底下有一群小弟,试想这样一个情景: + +> 一天,GPU 老大 收到 代理商 送过来的 小纸条 ,立马召集小弟,开始吩咐 +> +> :”唔~,这第一条,把翻斗大街的小广告都撕掉,张三,你去!“ +> +> ”收到!“,说完张三就立马向着翻斗大街扬长而去。 +> +> :”这第二条是~,把翻斗大街都贴满开发者的小广告,李四,你来!“ +> +> “得令!”,说完李四就骑上它心爱的小摩托,往翻斗大街飞驰而去。 +> +> 翌日,开发者带着一群小弟过来,找GPU老大,质问小广告怎么都没贴上,眼看就要掀桌子了。 +> +> GPU 老大 拿出 小纸条一对,没毛病呀,一问,张三也撕了,李四也贴了,再一问,卧槽,李四去早了,他先贴的小广告,后面全被张三给撕了。 +> +> 回顾一下,开发者因为没有做明确的说明,GPU老大的理解也不够正确,从而做了错误的安排,导致了这次事故的发生。 + +在GPU中,由于很多工作都是并行的,这也意味着可能会出现,某些操作按顺序启动,乱序执行的情况。 + +还有就是,CPU和GPU是互不等待的,但有些情况下,我们又想要能够等待某些操作执行结束或者事件触发。 + +为此,现代图形API就提供了一些结构来控制CPU,GPU,指令,队列之间的同步,以Vulkan为例,它就提供了几种用于控制同步的结构: + +- 栅栏(Fence) +- 信号量(semaphore) +- 屏障(Barrier) +- 事件(Event) + +关于它们的细节,可以阅读: + +- [vulkan中的同步和缓存控制之一,fence和semaphore](https://zhuanlan.zhihu.com/p/24817959) +- [vulkan中的同步和缓存控制之二,barrier和event](https://zhuanlan.zhihu.com/p/80692115) +- [理解Vulkan同步(Synchronization)](https://zhuanlan.zhihu.com/p/625089024) + +### 窗口表面(Window Surface) + +为了将渲染结果呈现到屏幕上,开发者往往还需要创建图形API与窗体系统之间的连接,在Vulkan中,可以通过为对应平台上的窗口创建 **窗口表面(Window Surface)** 来作为图形API与窗体系统交互的桥梁。 + +由于各个操作系统(Windows,Linux,Mac...)的窗口系统并不一致,所以Window Surface的创建有一些特定于平台的配置。 + +一些开源库致力于避开这些繁琐的配置,就比如 [GLFW](https://github.com/glfw/glfw) + +> **GLFW** 是用于 OpenGL、OpenGL ES 和 Vulkan 应用程序开发的开源、多平台库。 +> +> 它提供了一个简单轻量的、独立于平台的 API,用于创建窗口、上下文和表面、读取输入、处理事件等。 + +GUI 框架也大多提供了 Window Surface 的管理,我们要做的就是翻阅文档,深入源码。 + +### 交换链([SwapChain](https://en.wikipedia.org/wiki/Swap_chain)) + +在前两节中,我们已经通过控制台程序阐述了交换链的作用,它能有效的避免窗口绘制的闪烁和撕裂。 + +![img](Resources/Ch10_gpu_swapchain_display.png) + +> https://mkblog.co.kr/vulkan-tutorial-10-create-swap-chain/ + +### 流水线(Pipeline) + +流水线也叫做管线,它是在GPU上执行的任务流程,在现代计算机图形API中,常见的管线有三类: + +- 图形渲染管线([Graphics Pipeline](https://en.wikipedia.org/wiki/Graphics_pipeline)):用于在 **渲染目标(RenderTarget)** 上绘制图像 +- 计算管线([Compute Pipeline](https://en.wikipedia.org/wiki/Pipeline_(computing))):用于处理 **缓冲区(Buffer)** 或者 **图像(Image)** 中的数据 +- 光线追踪管线([Ray Tracing Pipeline](https://www.khronos.org/blog/ray-tracing-in-vulkan#blog_Ray_Tracing_Pipelines)):用于光线追踪流程 + +上一节中我们通过控制台程序以及简单的了解了一下图形渲染管线,这里有一个更完整的流程图: + +![图形流水线](Resources/graphics-pipeline.png) + +> https://graphicscompendium.com/intro/01-graphics-pipeline + +它的工作流程中可以简单当作是: + +- 开发者将顶点数据上传到GPU中 +- 每个顶点的数据都会被 **顶点着色器(Vertex Shader)** 处理 +- (如果有)再经过 **镶嵌控制着色器(Tessellation Control Shader)** , **镶嵌评估着色(Tessellation Evaluation Shader)** , **几何着色器(Geometry Shader)** 处理后,成功组装好三角形数据。 +- 之后 光栅化引擎 会将 三角形数据 映射(光栅化) 得到一个个 **片段(Frament)** +- 每个片段的数据都会被 **片段着色器(Fragment Shader)** 处理 +- 经由 **测试与混合阶段** 处理重叠片段 +- 得到对应坐标像素的唯一值,将之绘制到 **渲染目标(RenderTarget)** 上 + +### 着色器([Shader](https://en.wikipedia.org/wiki/Shader)) + +着色器是一种运行在GPU上的微小程序,它们主要用于扩展管线的功能,比如上面的图形渲染管线中: + +- 输入的顶点数组被 **顶点着色器(Vertex Shader)** 进行处理 +- 光栅化之后,每个像素又被 **片段着色器(Fragment Shader )** 处理 + +一条图形渲染管线要求至少有一个顶点着色器和片段着色器,但实际上,现代图形API还支持额外的着色器扩展: + +- **镶嵌控制着色器(Tessellation Control Shader)** +- **镶嵌评估着色器(Tessellation Evaluation Shader)** +- **几何着色器(Geometry Shader)** +- **计算着色器(Compute Shader)** + +它们使用其API特有的 **Shader Language** 进行编写,比如: + +- [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language):用于OpenGL、Vulkan +- [HLSL](https://en.wikipedia.org/wiki/High-Level_Shader_Language):用于Direct3D +- [MSL](https://en.wikipedia.org/wiki/Metal_(API)):用于Metal + +现代图形引擎的通用RHI中,由于要支持多种图形API,它们往往会使用上面的一种着色器语言进行开发,再转译成其他API的着色器代码。 + +> 在QRhi中,使用Vulkan风格的GLSL进行开发,Qt会通过glslang将GLSL编译为 [SPIR-V](https://zhuanlan.zhihu.com/p/497460602) 格式的着色器代码,再使用开源库 [SPIRV-Cross](https://github.com/KhronosGroup/SPIRV-Cross) 将之转译为各个API下的着色器代码,流程如下: +> +> ![着色器调节](Resources/shaderconditioning.pngwidth=1280&name=shaderconditioning-1682759360241-2.png) +> +> QRhi使用Vulkan风格的GLSL,因此这个文档是非常重要的: +> +> - https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html + +### 缓冲区(Buffer) + +这里的Buffer,并不只是指GPU上的一段内存(Memory) + +确切的说,一个Buffer不仅持有一段内存,还包含了自身的 **类型(Type)** 和 **用途(Usage)** + +在 **类型(Type)** 上,图形API一般将其划分为: + +- **Host Local Memory** :只对Host可见的内存,通常称之为普通内存 +- **Device Local Memory** :只对Device可见的内存,通常称之为显存 +- **Host Local Device Memory** :由Host管理的,对Device看见的内存 +- **Device Local Host Memory** :由Device管理的,对Host可见的内存 + +> Host 往往是 CPU端, Device 指 GPU 端 + +关于细节,请查阅: + +- [Vulkan 内存管理](https://zhuanlan.zhihu.com/p/166387973) + +使用合适的内存类型能大幅提升内存的读写效率,在现代图形引擎中的流水线中,会尽可能地使用 **Device Memory** ,当我们要从CPU中提交数据给它时,由于Host无法访问,一般会将数据上传到一个 **Host可见的Memroy** 上,再通过指令拷贝到对应的 **Device Memory** 上,我们一般称这个持有主机可见内存的Buffer为 [Staging Buffer](https://vulkan-tutorial.com/Vertex_buffers/Staging_buffer) + +> QRhi对这些类型进行了进一步包装,通过从使用意图上进行分类: +> +> - **Immutable** :用于存放希望永远不会发生改变的数据,具有非常高效的GPU读写性能,它通常放置在 **Devices Local** 的 GPU 内存上,无法被CPU直接读写,但QRhi却支持它的上传,其原理是:每次上传数据新建一个 **Host Local** 的 **Staging Buffer** 作为中转来上传新数据,这样操作的代价是非常高昂的。 +> - **Static** :同样存储在 **Devices Local** 的 GPU 内存上,与 Immutable 不同的是,首次上传数据创建的 **Staging Buffer** 会一直保留。 +> - **Dynamic** :用于存放频繁发生变化的数据,它放置 **Host Local** 的GPU内存中,为了不拖延图形渲染管线,它通常会使用双缓冲机制。 + +在 **用途(Usage)** 上,通常可具备以下 **标识(Flag)** : + +- **VertexBuffer** :用于存放顶点数据 +- **IndexBuffer** :用于存放索引数据 +- **UniformBuffer** :用于存储常量数据 +- **StorageBuffer** :用于Compute管线中的数据计算 +- **IndirectDrawBuffer** :用于间接渲染提供渲染参数 + +### 纹理([Texture](https://en.wikipedia.org/wiki/Image_texture))和采样器(Sampler) + +纹理持有一段具有像素结构的内存(Memory),它可以作为图形渲染管线的输入,或者RenderTarget的附件。 + +采样器用于定义纹理的才有规则,常见的规则有: + +- 在UV值超出值域[0,1]时,使用何种方式处理越界采样: + +![img](Resources/texture_wrapping.png) + +- 在精度过高或过低时,使用何种方式对纹理进行采用: + +![img](Resources/texture_filtering.png) + +> 这里分别对应QRhi中的 **QRhiTexture** 和 **QRhiSampler** +> +> ``` c++ +> /*创建带有一个颜色附件的RT*/ +> QSharedPointer colorAttachment; +> colorAttachment.reset(rhi->newTexture(QRhiTexture::RGBA32F, QSize(800, 600), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); +> colorAttachment->create(); +> QSharedPointer renderTarget; +> QSharedPointer renderPassDesc; +> renderTarget.reset(rhi->newTextureRenderTarget({ colorAttachment.get() })); +> renderPassDesc.reset(renderTarget->newCompatibleRenderPassDescriptor()); +> renderTarget->setRenderPassDescriptor(renderPassDesc.get()); +> renderTarget->create(); +> ``` +> + +### 描述符(Descriptor) + +上面图形渲染管线中说到,每个顶点都会被顶点着色器处理,每个片段都会被片段着色器处理,`每个` 英语译作 `Per` ,在流水线中,除了这类 `Per` 数据,往往还有相同 着色器之间 公共的,共享的 数据,这类数据,在图形API中,我们一般称作 Uniform 数据,它可以当作是着色器的公共输入。 + +在创建流水线结构的时候,就需要使用描述符(Descriptor)来描述Uniform数据的基本结构信息,常见的描述符类型有: + +- **UniformBuffer** :着色器只读的Buffer输入 +- **Sampler** :着色器只读的纹理采样输入 +- **StorageBuffer** :着色器可读可写的Buffer,在计算管线中使用 +- **StorageImage** :着色器可读可写的Image,在计算管线中使用 + +> 这里对应QRhi的 **QRhiShaderResourceBinding** : +> +> ![image-20230430003736149](Resources/image-20230430003736149.png) + +### 描述符集布局绑定(DescriptorSetLayoutBinding) + +一个着色器往往不会只有一种类型的Uniform输入,描述符集布局绑定(DescriptorSetLayoutBinding)是一个描述符的集合,它定义了着色器上的Uniform结构布局,图形API在创建流水线之后,流水线的绝大部分参数都无法改变,而描述符集布局绑定,是可以在不重建流水线的前提下,动态去替换的。 + +> 这里对应QRhi的 **QRhiShaderResourceBindings** ,它的创建方式非常简单: +> +> ``` elm +> mShaderBindings.reset(mRhi->newShaderResourceBindings()); +> mShaderBindings->setBindings({ +> QRhiShaderResourceBinding::bufferLoadStore(0,QRhiShaderResourceBinding::ComputeStage,mStorageBuffer.get()), +> QRhiShaderResourceBinding::imageLoadStore(1,QRhiShaderResourceBinding::ComputeStage,mTexture.get(),0), +> }); +> mShaderBindings->create(); +> ``` + +### 渲染目标([RenderTarget](https://en.wikipedia.org/wiki/Render_Target)) + +它等价于OpenGL中的 [Frame Buffer Object](https://en.wikipedia.org/wiki/Framebuffer_object),它由一个或多个 **颜色附件(Color Attachment)** 组成,可能包含深度附件和模板附件。 + +![Esquema representando a estrutura de um Frame Buffer Object [Green 2005] ](Resources/Figura-5-Esquema-representando-a-estrutura-de-um-Frame-Buffer-Object-Green-2005.png) + +通常,我们的绘制其实是在RenderTarget上填充颜色数据,对于深度和模板的数据,GPU会自动进行处理,我们只需要去配置一些处理规则。 + +> 它对应QRhi中的 **QRhiRenderTarget** ,使用QRhi创建一个RenderTarget是一件非常容易的事情: +> +> ``` c++ +> /*创建带有一个颜色附件的RT*/ +> QSharedPointer colorAttachment; +> colorAttachment.reset(rhi->newTexture(QRhiTexture::RGBA32F, QSize(800, 600), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); +> colorAttachment->create(); +> QSharedPointer renderTarget; +> QSharedPointer renderPassDesc; +> renderTarget.reset(rhi->newTextureRenderTarget({ colorAttachment.get() })); +> renderPassDesc.reset(renderTarget->newCompatibleRenderPassDescriptor()); +> renderTarget->setRenderPassDescriptor(renderPassDesc.get()); +> renderTarget->create(); +> ``` + +### 渲染通道(RenderPass) + +RenderPass可以当作是一次或多次Pipeline的执行过程 + +在游戏和影视作品中,为朴素的图形增加一些后期效果,能极大程度的提升画面的艺术感,而在图形渲染管线中,Pass可以当作是一帧图像(RenderTarget)的绘制。 + +在游戏引擎中,绘制朴素几何物体的Pass我们一般称为 **BasePass** + +下面的 **FrameGraph** 展示了如何使用多个Pass来实现[Bloom](https://en.wikipedia.org/wiki/Bloom_(shader_effect))的效果 + +![image-20230305215210666](Resources/image-20230305215210666.png) + +### 渲染通道描述符(RenderPassDescriptor ) + +RenderPassDescriptor用于规定何种格式的RenderTarget符合该RenderPass的使用,在Vulkan中,它的创建极其繁琐。在QRhi,只需要使用: + +``` c++ +class QRhiTextureRenderTarget : public QRhiRenderTarget +{ + virtual QRhiRenderPassDescriptor* newCompatibleRenderPassDescriptor() = 0; +}; +``` + +## QRhi + +如果你是一名初学者,看了上面的基础概念,可能这个时候脑袋瓜已经嗡嗡的了,不过放心,在你上手代码之后,这些概念都是纸老虎,在本大章的接下来的内容中,将系统地阐述图形API基础的结构和使用方式。 + +首先,你需要Qt,可以在这个 镜像源 下载Qt的在线下载器: + +- http://mirrors.ustc.edu.cn/qtproject/official_releases/online_installers/ + +如果你已经有Qt了,可以在Qt的安装目录找到它的维护工具,它用于更新和卸载Qt的组件 + +![image-20230429172832427](Resources/image-20230429172832427.png) + +请确保至少安装以下Qt组件: + +- **6.0以上版本的Qt库** (当前最新 `6.6.0`) + - **MSVC编译套件** (当前最新 `2019 64-bit` ) 或 **MinGW编译套件** (当前最新 `11.2.0 64- bit`):如果IDE是Visual Studio,则需要下载MSVC。 + - **Sources** :Qt的源码 + - **Qt 5 Compatibility Module** :Qt5的兼容模块 + - **Qt Shader Tools** :Qt的着色器编译工具 + - **Additional Libraries** :Qt的扩展库 + - **Qt Multimedia** :Qt的多媒体模块,里面有音视频编解码的功能封装 + - **Qt Debug Information Files(可选)** :用于MSVC下调试Qt的源码,有十几G,比较大 + +- **Developer and Designer Tools** + - **Debugging Tools for Windows** :用于MSVC下的调试。 + - **CMake** (当前最新 `3.24.2`) + +此外,还需要安装 [Vulkan SDK](https://www.lunarg.com/vulkan-sdk/) + +你可以通过一个小工程来验证环境是否正常: + +- 新建一个`CMakeLists.txt`和`main.cpp` + +- 在`CMakeLists.txt`中创建一个`Target`并链接Qt的模块: + +``` cmake +cmake_minimum_required(VERSION 3.12) + +project(FirstRhiWindowProj) + +add_executable(FirstRhiWindow main.cpp) + +find_package(Qt6 COMPONENTS Core Widgets Gui REQUIRED) + +target_link_libraries(FirstRhiWindow + PRIVATE + Qt6::Core + Qt6::Widgets + Qt6::Gui + Qt6::GuiPrivate //qrhi是GUI的私有模块 +) +``` + +- 在`main.cpp`中填入以下代码: + +```C++ +#include +#include + +int main(int argc, char** argv) { + QRhiD3D11InitParams params; + QRhi::Flags flags; + QSharedPointer rhi(QRhi::create(QRhi::D3D11, ¶ms, flags)); + qDebug() << rhi->driverInfo(); //打印显卡设备信息 + return 0; +} +``` + +- 使用cmake编译执行,执行成功你能看到控制台窗口打印了显卡信息: + + ![image-20230429175841296](Resources/image-20230429175841296.png) + +### QRhiWindow + +操作系统提供的窗口有非常细粒度的控制,在开发者想要调整窗口的某个效果时,可能会需要改变窗口的多个属性和事件,当大量的效果堆叠设置的时候,就很比较容易出现冲突。打个比方: + +> 假如 **A** 效果需要开启属性`a`和`b`, **B** 效果需要开启属性`b`和`c`,如果窗口开启了效果 **A** 和 **B** (即开启属性`a`、`b`、`c`),之后如果想要关闭效果 **B** ,如果不稍加验证,直接关闭了与之关联的属性`b`和`c`,那么开启的属性只剩下`a`,效果 **A** 的显示就会出现问题。 + +开发者往往只希望关注窗口的表现效果,而不是操作系统级别的窗口属性和事件,所以在窗口管理中,效果间的设置就存在了大量属性和事件的重叠 + +如果给每个属性的设置都加一遍关联验证,会使得代码变得很臃肿,且难以维护,也更容易出Bug + +因此Qt采用了状态机的方式来跟管理原生窗口: + +- 在Qt层面对Window的各种设置并没有立即生效,而是将这些设置存储起来,等到特定时机,比如说Show,Qt才会根据当前上下文的配置来创建实际的操作系统窗口 + +换而言之,对于Qt的窗口,只有在调用了show函数之后,才实际创建了Window,才能拿到它的窗口句柄。 + +> 在程序开发过程中,经常可以接触到这类代码,它能很好地组织复杂的代码逻辑,尤其是在图形相关的工程代码中,经常能看到以`ensure`开头的函数,而它所做的工作,就是根据当前属性上下文来进行某些操作。 + +而我们如果要创建窗口的图形渲染结构,由于需要实际的窗口句柄,也必须在show之后进行初始化,不过好在Qt提供了相关的事件 : + +- [void QWindow:: exposeEvent (QExposeEvent* ev )](https://doc.qt.io/qt-6/qwindow.html#exposeEvent) + - 当窗口从 `un-exposed` 切换到 `exposed` 状态时,会执行此事件 + - 在第一次显示窗口时,会在此事件之前执行resizeEvent + +> 需要注意的是,对于一个窗口而言,exposeEvent会多次执行,但初始化只需要进行一次,因此需要增加一些逻辑验证。 + +而图形渲染结构主要指的是: + +- `QScopedPointer mRhi` +- `QScopedPointer mSwapChain`:交换链 +- `QScopedPointer mDSBuffer`:深度模板缓冲区 +- `QScopedPointer mSwapChainPassDesc`:交换链的RenderPass描述符 + +成功初始化之后,我们只需要在窗口上增加渲染和Resize的逻辑即可,这里有一个标准的Window实现示例: + +- https://github.com/qt/qtbase/blob/dev/tests/manual/rhi/shared/examplefw.h + +还需要注意的是,使用QRhi创建的资源,必须在QRhi销毁之间进行清理,使用智能指针可以很好的管理这些资源的生命周期。 + +### QRhiWidget + +Widget与Window最大的不同是: + +- Widget通过某些事件来触发界面的重绘,而Window则是每帧都在重绘 + +使用Widget能更好的节约性能,避免不必要的刷新,但在游戏和图形这种需要实时刷新的界面时,使用Window能更好地分摊绘制性能。 + +这里有一个标准的Widget实现: + +- https://github.com/qt/qtbase/tree/dev/tests/manual/rhi/rhiwidget + +### 文章示例 + +你可以该仓库下找到该系列文章的所有文档和代码: + +- https://github.com/Italink/ModernGraphicsEngineGuide + +可以参考如下视频进行构建: + +- https://www.bilibili.com/video/BV1VP41127UM + +它里面包含很多有意思的示例,如果你不想进行构建,这里有一个编译好的可执行文件包: + +- https://github.com/Italink/ModernGraphicsEngineGuide/releases/download/v1.0.0/Release.zip + +#### QEngineUtilities + +为了能够简化一些结构的使用,笔者把一些通用的结构封装到了[QEngineUtilities](https://github.com/Italink/QEngineUtilities/)中,它包含三个构建目标: + +- **QEngineCore** :渲染架构,包含RHI、FrameGraph、RenderPass、RenderComponent、Asset的简易封装。 +- **QEngineEditor** :编辑器套件,包含了许多基础属性调整控件,比如:数值调节器,调色器,通知气泡,基于QObject的DetailView,GLSL代码编辑器等。 +- **QEngineUtilities** :Lanuch层,在不同构建配置下对上面两个模块进行组装,例如在`DebugEditor`配置下,会嵌入编辑器界面,而在`Debug`配置下,就只有Core模块。 + +你可以把这个仓库Pull下来: + +- 在自己工程的CMakeLists.txt中使用[add_subdirectory](https://cmake.org/cmake/help/latest/command/add_subdirectory.html) 添加 **QEngineUtilities** +- 并且给自己的`Target` 链接 **QEngineUtilities** +- 在cmake中使用函数`qengine_setup_env`为自己的`Target`配置环境变量(添加dll搜索路径) + +它主要用于教学和尝试: + +- 强调可读性是第一要素 +- 没有细致地追求性能(代码细节上有一些瑕疵,在笔者察觉到的时候已经太晚了,由于精力有限,目前笔者也只能选择妥协,非常抱歉...不过放心,这些影响微乎其微) +- 以渲染为核心,包含少量编辑器架构,不会引入一些会导致代码膨胀的模块,如资产管理,网络,异步,ECS... + +如果成功,你可以使用如下代码来创建Widget和Window: + +``` c++ +#include +#include "Render/RHI/QRhiWidget.h" +#include "Render/RHI/QRhiWindow.h" + +class ExampleRhiWindow : public QRhiWindow { +public: + ExampleRhiWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) {} +protected: + virtual void onRenderTick() override { + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 1.0f, 1.0f); //使用蓝色清屏 + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue); + cmdBuffer->endPass(); + } +}; + +class ExampleRhiWidget : public QRhiWidget { +public: + ExampleRhiWidget() { + setDebugLayer(true); //开启验证层 + } + void render(QRhiCommandBuffer* inCmdBuffer) override { + const QColor clearColor = QColor::fromRgbF(1.0f, 0.0f, 0.0f, 1.0f); //使用红色清屏 + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + inCmdBuffer->beginPass(mRenderTarget.data(), clearColor, dsClearValue); + inCmdBuffer->endPass(); + } +}; + +int main(int argc, char **argv){ + QApplication app(argc, argv); + + QRhiWindow::InitParams initParams; + ExampleRhiWindow window(initParams); + window.setTitle("01-RhiWindow"); + window.resize({ 400,400 }); + window.show(); + + ExampleRhiWidget widget; + widget.setWindowTitle("01-RhiWidget"); + widget.setApi(QRhiWidget::Vulkan); + widget.resize({ 400,400 }); + widget.show(); + + return app.exec(); +} +``` + +运行它可以看到: + +![image-20230304201240208](Resources/image-20230304201240208.png) + +如果遇到困难,可以参考 [ModernGraphicsEngineGuide](https://github.com/Italink/ModernGraphicsEngineGuide/tree/main/Source/1-GraphicsAPI) 中的 **01-WindowAndWidget** 项目,也可以在[此处](https://github.com/Italink/ModernGraphicsEngineGuide/issues)提问 + +下一章节,我们开始整活~ diff --git "a/Docs/01-GraphicsAPI/1.\346\270\262\346\237\223\347\252\227\345\217\243.md" "b/Docs/01-GraphicsAPI/1.\346\270\262\346\237\223\347\252\227\345\217\243.md" deleted file mode 100644 index f3a4d7a1..00000000 --- "a/Docs/01-GraphicsAPI/1.\346\270\262\346\237\223\347\252\227\345\217\243.md" +++ /dev/null @@ -1,343 +0,0 @@ -# 渲染窗口 - -在计算机上,窗口是图形的载体,在GUI章节中,我们简单了解了窗口的概念,下面将开始使用Qt封装好的窗口搭建基本的图形渲染结构: - -新建一个`CMakeLists.txt`和`main.cpp` - -在`CMakeLists.txt`中创建一个`Target`并链接Qt的模块: - -``` cmake -cmake_minimum_required(VERSION 3.12) - -project(FirstRhiWindowProj) - -add_executable(FirstRhiWindow main.cpp) - -find_package(Qt6 COMPONENTS Core Widgets Gui REQUIRED) - -target_link_libraries(FirstRhiWindow - PRIVATE - Qt6::Core - Qt6::Widgets - Qt6::Gui - Qt6::GuiPrivate //qrhi是GUI的私有模块 -) -``` - -使用QRhi,需要包含: - -``` c++ -#include -``` - -下面的代码可以创建一个以DX11进行渲染的QRhi,可以用于测试环境是否正常: - -```C++ -#include -#include - -int main(int argc, char** argv) { - QRhiD3D11InitParams params; - QRhi::Flags flags; - QSharedPointer rhi(QRhi::create(QRhi::D3D11, ¶ms, flags)); - qDebug() << rhi->driverInfo(); //打印显卡设备信息 - return 0; -} -``` - -> 如果想让 **Vulkan正常工作** ,需要下载 [Vulkan SDK](https://www.lunarg.com/vulkan-sdk/) - -## QRhiWindow - -操作系统提供的窗口有非常细粒度的控制,在开发者想要调整窗口的某个效果时,可能会需要改变窗口的多个属性和事件,当大量的效果堆叠设置的时候,就很比较容易出现冲突。打个比方: - -> 假如 **A** 效果需要开启属性`a`和`b`, **B** 效果需要开启属性`b`和`c`,如果窗口开启了效果 **A** 和 **B** (即开启属性`a`、`b`、`c`),之后如果想要关闭效果 **B** ,如果不稍加验证,直接关闭了与之关联的属性`b`和`c`,那么开启的属性只剩下`a`,效果 **A** 的显示就会出现问题。 - -开发者往往只希望关注窗口的表现效果,而不是操作系统级别的窗口属性和事件,所以在窗口管理中,效果间的设置就存在了大量属性和事件的重叠 - -如果给每个属性的设置都加一遍关联验证,会使得代码变得很臃肿,且难以维护,也更容易出Bug - -因此Qt采用了状态机的方式来跟管理原生窗口: - -- 在Qt层面对Window的各种设置并没有立即生效,而是将这些设置存储起来,等到特定时机,比如说Show,Qt才会根据当前上下文的配置来创建实际的操作系统窗口 - -换而言之: - -- 对于Qt的窗口,只有在调用了show函数之后,才实际创建了Window,才能拿到它的窗口句柄 - -而我们如果要创建窗口的图形渲染结构,由于需要实际的窗口句柄,也必须在show之后进行初始化,不过好在Qt提供了相关的事件 : - -- [void QWindow:: exposeEvent (QExposeEvent* ev )](https://doc.qt.io/qt-6/qwindow.html#exposeEvent) - - 当窗口从 `un-exposed` 切换到 `exposed` 状态时,会执行此事件 - - 在第一次显示窗口时,会在此事件之前执行resizeEvent - -> 需要注意的是,对于一个窗口而言,exposeEvent会多次执行,但初始化只需要进行一次,因此需要增加一些逻辑验证。 - -而图形渲染结构主要指的是: - -- `QScopedPointer mRhi` -- `QScopedPointer mSwapChain`:交换链 -- `QScopedPointer mDSBuffer`:深度模板缓冲区 -- `QScopedPointer mSwapChainPassDesc`:交换链的RenderPass描述符 - -成功初始化之后,我们只需要在窗口上增加渲染和Resize的逻辑即可,这里有一个标准的Window实现示例: - -- https://github.com/qt/qtbase/blob/dev/tests/manual/rhi/shared/examplefw.h - -还需要注意的是,使用QRhi创建的资源,必须在QRhi销毁之间进行销毁,使用智能指针可以很好的管理这些资源的生命周期。 - -## QRhiWidget - -Widget与Window最大的不同是: - -- Widget通过某些事件来触发界面的重绘,而Window则是每帧都在重绘 - -使用Widget能更好的节约性能,避免不必要的刷新,但在游戏和图形这种需要实时刷新的界面时,使用Window能更好地分摊绘制性能。 - -这里有一个标准的Widget实现: - -- https://github.com/qt/qtbase/tree/dev/tests/manual/rhi/rhiwidget - -## 封装 - -为了能够简化上述结构的使用,笔者在[QEngineUtilities](https://github.com/Italink/QEngineUtilities/)中做了一些简单的封装,你可以把这个仓库Pull下来: - -- 在CMakeLists.txt中使用[add_subdirectory ](https://cmake.org/cmake/help/latest/command/add_subdirectory.html)添加 **QEngineUtilities** -- 并且给 `Target` 链接 **QEngineUtilities** - -如果成功,你可以使用如下代码来创建Widget和Window: - -``` c++ -#include -#include "Render/RHI/QRhiWidget.h" -#include "Render/RHI/QRhiWindow.h" - -class ExampleRhiWindow : public QRhiWindow { -public: - ExampleRhiWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) {} -protected: - virtual void onRenderTick() override { - QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 1.0f, 1.0f); //使用蓝色清屏 - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - - cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue); - cmdBuffer->endPass(); - } -}; - - -class ExampleRhiWidget : public QRhiWidget { -public: - ExampleRhiWidget() { - setDebugLayer(true); //开启验证层 - } - void render(QRhiCommandBuffer* inCmdBuffer) override { - const QColor clearColor = QColor::fromRgbF(1.0f, 0.0f, 0.0f, 1.0f); //使用红色清屏 - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - inCmdBuffer->beginPass(mRenderTarget.data(), clearColor, dsClearValue); - inCmdBuffer->endPass(); - } -}; - -int main(int argc, char **argv){ - QApplication app(argc, argv); - - QRhiWindow::InitParams initParams; - ExampleRhiWindow window(initParams); - window.setTitle("01-RhiWindow"); - window.resize({ 400,400 }); - window.show(); - - ExampleRhiWidget widget; - widget.setWindowTitle("01-RhiWidget"); - widget.setApi(QRhiWidget::Vulkan); - widget.resize({ 400,400 }); - widget.show(); - - return app.exec(); -} -``` - -运行它可以看到: - -![image-20230304201240208](Resources/image-20230304201240208.png) - -如果遇到困难,可以参考 [ModernGraphicsEngineGuide](https://github.com/Italink/ModernGraphicsEngineGuide/tree/main/Source/1-GraphicsAPI) 中的 **01-WindowAndWidget** 项目 - -## 基础概念 - -上面的代码中出现了很多莫名其妙的结构,暂时不用去深入考虑它们的意义,这些名词只不过是给一些特定结构和流程一个能够称呼的简短昵称,当你了解它的应用场景和熟悉它的使用流程,自然会明白它们的意义。 - -这里会做一个简单的介绍,在后面的章节中会有更多细节。 - -### 交换链([SwapChain](https://en.wikipedia.org/wiki/Swap_chain)) - -在前两节中,我们通过控制台程序阐述了交换链的作用,它能有效的避免窗口绘制的闪烁和撕裂。 - -![img](Resources/Ch10_gpu_swapchain_display.png) - -> https://mkblog.co.kr/vulkan-tutorial-10-create-swap-chain/ - -QWidget拥有一套自己的SwapChain机制,而在QRhiWindow中,需要我们通过 **QRhiSwapChain** 来获取当前的操作对象。 - -### 流水线(Pipeline) - -也叫做管线,它们在GPU上运行,在计算机图形中,有两类管线: - -- 图形渲染管线([Graphics Pipeline](https://en.wikipedia.org/wiki/Graphics_pipeline)):将一系列数据转换为图形的过程 -- 计算管线([Compute Pipeline](https://en.wikipedia.org/wiki/Pipeline_(computing))):对一系列数据进行处理的过程 - -上一节中我们通过控制台程序以及简单的了解了一下图形渲染管线,而这里有一个更完整的流程图: - -![图形流水线](Resources/graphics-pipeline.png) - -> https://graphicscompendium.com/intro/01-graphics-pipeline - -### 着色器([Shader](https://en.wikipedia.org/wiki/Shader)) - -着色器是一种运行在GPU上的微小程序,它们主要用于扩展管线的功能,比如上面的图形渲染管线中: - -- 输入的顶点数组被 **顶点着色器(Vertex Shader)** 进行处理 -- 光栅化之后,每个像素又被 **片段着色器(Fragment Shader )** 处理 - -一条图形渲染管线要求至少有一个顶点着色器和片段着色器,但实际上,现代图形API还支持额外的着色器扩展: - -- **镶嵌控制着色器(Tessellation Control Shader)** -- **镶嵌评估着色器(Tessellation Evaluation Shader)** -- **几何着色器(Geometry Shader)** -- **计算着色器(Compute Shader)** - -它们使用其API特有的 **Shader Language** 进行编写,比如: - -- [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language):用于OpenGL、Vulkan -- [HLSL](https://en.wikipedia.org/wiki/High-Level_Shader_Language):用于Direct3D -- [MSL](https://en.wikipedia.org/wiki/Metal_(API)):用于Metal - -QRhi使用Vulkan风格的GLSL,因此这个文档是非常重要的: - -- https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html - -### 缓冲区(Buffer) - -这里的Buffer是指GPU上的一段内存(Memory) - -在 **类型(Type)** 上,QRhi将之划分为: - -- **Immutable** :用于存放希望永远不会发生改变的数据,具有非常高效的GPU读写性能,它通常放置在 **Devices Local** 的 GPU 内存上,无法被CPU直接读写,但QRhi却支持它的上传,其原理是:每次上传数据新建一个 **Host Local** 的 **Staging Buffer** 作为中转来上传新数据,这样操作的代价是非常高昂的。 -- **Static** :同样存储在 **Devices Local** 的 GPU 内存上,与 Immutable 不同的是,首次上传数据创建的 **Staging Buffer** 会一直保留。 -- **Dynamic** :用于存放频繁发生变化的数据,它放置 **Host Local** 的GPU内存中,为了不拖延图形渲染管线,它通常会使用双缓冲机制。 - -关于 **Host Local** 和 **Devices Local** 可以查阅: - -- https://zhuanlan.zhihu.com/p/166387973 - -在 **用途(Usage)** 上,可以具备如下标识: - -- **VertexBuffer** :用于存放顶点数据 -- **IndexBuffer** :用于存放索引数据 -- **UniformBuffer** :用于存储常量数据 -- **StorageBuffer** :用于Compute管线中的数据计算 -- **IndirectDrawBuffer** :用于间接渲染提供渲染参数 - -### 纹理([Texture](https://en.wikipedia.org/wiki/Image_texture))和采样器(Sampler) - -纹理可以当作是一张存储在GPU上的图像,它往往作为图形渲染管线的参数,或者作为RenderTarget的附件 - -采样器用于: - -- 在UV值超出值域[0,1]时,使用何种方式处理越界采样: - -![img](Resources/texture_wrapping.png) - -- 在精度过高或过低时,使用何种方式对纹理进行采用: - -![img](Resources/texture_filtering.png) - -> https://learnopengl.com/Getting-started/Textures - -分别对应QRhi中的 **QRhiTexture** 和 **QRhiSampler** - -### 渲染目标([RenderTarget](https://en.wikipedia.org/wiki/Render_Target)) - -它等价于OpenGL中的 [Frame Buffer Object](https://en.wikipedia.org/wiki/Framebuffer_object),它由一个或多个颜色附件组成,可能包含深度附件和模板附件。 - -![Esquema representando a estrutura de um Frame Buffer Object [Green 2005] ](Resources/Figura-5-Esquema-representando-a-estrutura-de-um-Frame-Buffer-Object-Green-2005.png) - -通常,我们的绘制其实就是在RenderTarget上填充颜色数据,对于深度和模板的数据,GPU会自动进行处理,我们只需要去配置一些处理规则。 - -它对应Qt中的 **QRhiRenderTarget** ,在上面的ExampleRhiWindow示例中,对SwapChain中的当前RenderTarget使用蓝色进行清屏。 - -```c++ -virtual void onRenderTick() override { - QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 1.0f, 1.0f); - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - - cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue); - cmdBuffer->endPass(); -} -``` - -在QRhi中,创建一个RenderTarget是一件非常容易的事情: - -``` c++ -/*创建带有一个颜色附件的RT*/ -QSharedPointer colorAttachment; -colorAttachment.reset(rhi->newTexture(QRhiTexture::RGBA32F, QSize(800, 600), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); -colorAttachment->create(); -QSharedPointer renderTarget; -QSharedPointer renderPassDesc; -renderTarget.reset(rhi->newTextureRenderTarget({ colorAttachment.get() })); -renderPassDesc.reset(renderTarget->newCompatibleRenderPassDescriptor()); -renderTarget->setRenderPassDescriptor(renderPassDesc.get()); -renderTarget->create(); -``` - -### 命令缓冲区(CommandBuffer) - -CommandBuffer是现代图形API提出的概念,它用于存储GPU的操作指令,最后统一提交到GPU上进行处理 - -在QRhi中,不能直接创建CommandBuffer,得到它的途径有两种: - -- 通过 SwapChain 可以拿到当前的 CommandBuffer -- `rhi->beginOffscreenFrame` 会创建一个新的 CommandBuffer - -**QRhiCommandBuffer** 支持如下指令: - -![image-20230305213442549](Resources/image-20230305213442549.png) - -### 渲染通道(RenderPass) - -RenderPass可以当作是一次或多次Pipeline的执行过程 - -在游戏和影视作品中,为朴素的图形增加一些后期效果,能极大程度的提升画面的艺术感,而在图形渲染管线中,Pass可以当作是一帧图像(RenderTarget)的绘制。 - -在游戏引擎中,绘制朴素几何物体的Pass我们一般称为 **BasePass** - -下面的 **FrameGraph** 展示了如何使用多个Pass来实现[Bloom](https://en.wikipedia.org/wiki/Bloom_(shader_effect))的效果 - -![image-20230305215210666](Resources/image-20230305215210666.png) - -### 渲染通道描述符(RenderPassDescriptor ) - -RenderPassDescriptor用于规定何种格式的RenderTarget符合该RenderPass的使用,在Vulkan中,它的创建极其繁琐,而在QRhi,只需要使用: - -``` c++ -class QRhiTextureRenderTarget : public QRhiRenderTarget -{ - virtual QRhiRenderPassDescriptor* newCompatibleRenderPassDescriptor() = 0; -}; -``` - -## 尝试 - -如果你成功搭建了QRhi的渲染窗口,可以尝试复现上一节中QRhi中的示例工程,也可以运行一下 [ModernGraphicsEngineGuide](https://github.com/Italink/ModernGraphicsEngineGuide/tree/main/Source) 中的一些Demo - -如果你有其他图形API的基础,QRhi一定能让你爱不释手~ - -如果遇到问题,可以在[此处](https://github.com/Italink/ModernGraphicsEngineGuide/issues)提问 diff --git "a/Docs/01-GraphicsAPI/2.\345\233\276\345\275\242\346\270\262\346\237\223\347\256\241\347\272\277.md" "b/Docs/01-GraphicsAPI/2.\345\233\276\345\275\242\346\270\262\346\237\223\347\256\241\347\272\277.md" new file mode 100644 index 00000000..f0f1e1df --- /dev/null +++ "b/Docs/01-GraphicsAPI/2.\345\233\276\345\275\242\346\270\262\346\237\223\347\256\241\347\272\277.md" @@ -0,0 +1,419 @@ +--- +comments: true +--- + +# 图形渲染管线 + +上一节中,我们简单了解了图形渲染管线的概念,它的基本流程如下: + +![graphics-pipeline](Resources/graphics-pipeline.png) + +眼过千遍,不如手过一遍,确保你已经克隆了 [QEngineUtilities](https://github.com/Italink/QEngineUtilities) ,并将其链接到了自己的工程里面,由于我们的学习目标是图形渲染,而不是 UI ,所以 QRhiWindow 是一个更好的起点,它的核心结构如下: + +``` c++ +class QRhiWindow :public QWindow { +protected: + virtual void onInit(){} //初式化渲染资源之后会调用 + virtual void onRenderTick() {} //每帧都会调用 + virtual void onResize(const QSize& inSize) {} //当窗口尺寸发生变化时会调用 + virtual void onExit() {} //当关闭窗口时调用 +protected: + QSharedPointer mRhi; + QScopedPointer mSwapChain; + QScopedPointer mDSBuffer ; + QScopedPointer mSwapChainPassDesc; +}; +``` + +我们可以新建一个继承自QRhiWindow的类,通过覆写上述的几个虚函数和使用几个保护性成员变量,来实现自己的渲染逻辑。 + +## 渲染结构初始化 + +在开始渲染之前,通常需要创建一些用于渲染的结构,但需要注意的是:我们并不会把渲染结构的初始化放在 `onInit` 函数里面,而是放在 `onRenderTick` 里面,通过一个逻辑开关去控制初始化。 + +整个过程看起来就像是这样: + +``` c++ +class MyRhiWindow :public QRhiWindow { +private: + bool bNeedInit; +public: + MyRhiWindow() + :bNeedInit(true) + {} +protected: + virtual void onRenderTick() override { + if(bNeedInit){ + /* + * 执行初始化渲染资源的逻辑 + */ + + bNeedInit = false; // 清除初始化的开关 + } + } +}; +``` + +由于 `onRenderTick`是每帧执行的,这样做的好处是:当我们需要重建某些结构的时候,只需要把开关重新打开就行,而不用统一的调用 `onInit`。 + +QEngineUtilities中对这个结构做了一些简单的封装,能够让使用方式看上去更 **人性化** 一些,上面的代码可以等价替换为: + +``` c++ +class MyRhiWindow :public QRhiWindow { +private: + QRhiEx::Signal mSigInit; //初式化信号 +public: + MyRhiWindow(){ + mSigInit.request(); //请求初始化 + } +protected: + virtual void onRenderTick() override { + if(mSigInit.ensure()){ //确保初始化逻辑能执行 + /* + * 执行初始化渲染资源的逻辑 + */ + } + } +}; +``` + +### 创建顶点缓冲区 + +假如我们使用这样的顶点数据: + +``` c++ +static float VertexData[] = { //顶点数据 + //position(xy) color(rgba) + 0.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, + -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, +}; +``` + +> 顶点数据 并不单指 顶点位置,它可以是任何能影响几何表现效果的数据,常见的有:位置,颜色,法向量,UV(纹理坐标)等。 + +为了让图形渲染管线能够访问这些顶点数据,我们需要创建一个在GPU侧用于存储顶点数据的缓冲区(VertexBuffer): + +``` c++ +QScopedPointer mVertexBuffer; +``` + +```c++ +mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); +mVertexBuffer->create(); //在QRhi中,调用渲染资源对象的create函数才实际创建对应的GPU资源,在这之前,我们都是调整参数状态机而已 +``` + +### 创建顶点输入布局 + +为了让流水线能够解析顶点数据的结构,我们还需要创建一个描述顶点输入布局的结构—— **QRhiVertexInputLayout** : + +```c++ +QRhiVertexInputLayout inputLayout; +inputLayout.setBindings({ + QRhiVertexInputBinding(6 * sizeof(float)) +}); + +inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0 , QRhiVertexInputAttribute::Float2, 0), + QRhiVertexInputAttribute(0, 1 , QRhiVertexInputAttribute::Float4, sizeof(float) * 2 ), +}); +``` + +`setBindings` 用于描述每个 VertexBuffer 中,单个顶点数据的跨度。 + +- 由于我们只有一个VertexBuffer,所以只需要创建一个 **QRhiVertexInputBinding** ,并且由于我们在这个VertexBuffer中使用2个float表示位置,4个float表示颜色,所以单个顶点数据的跨度也就是 `6*sizeof(float)` + +`setAttributes` 用于确定流水线顶点数据中,每个属性的布局。 + +**QRhiVertexInputAttribute** 构造的关键参数有: + +- **binding(int)** :用于确定该顶点属性位于哪个QRhiVertexInputBinding(也就是从哪个VertexBuffer中去读取数据) +- **location(int)** :用于定义该顶点属性在着色器中的位置,它可以是乱序且任意的,但需要保证它的值在inputLayout中是唯一的,且没有超出硬件的限制 +- **format(Format)** :用于说明该顶点属性的数据类型,常见的比如Float,Float2,Float3,Float4... +- **offset(quint32)** :用于描述该顶点属性在单个`顶点数据`中内存的偏移 + +综上,我们创建了这样的顶点输入的布局描述: + +![image-20230501111914434](Resources/image-20230501111914434.png) + +如果在顶点着色器中使用它,它的结构定义必须是: + +```glsl +layout(location = 0) in vec2 position; //这里需要与上面的inputLayout对应,变量名可以是任意的 +layout(location = 1) in vec4 color; +``` + +### 创建流水线 + +在QRhi中,创建图形渲染管线非常简单,就像这样: + +``` c++ +QScopedPointer mPipeline; +``` + +``` c++ +mPipeline.reset(mRhi->newGraphicsPipeline()); +``` + +创建流水线需要我们至少配置: + +- 顶点输入布局(Vertex Input Layout) +- 着色器资源绑定(Shader Resource Bindings),也就是上一节所提到的 描述符集布局绑定 +- 顶点着色器(Vertex Shader)和片段着色器(Fragment Shader) +- 和 渲染目标(RenderTarget) 一致的 重采样数(SampleCount) 和 渲染通道描述(RenderPassDescriptor) + +首先,我们先装配之前创建好的顶点输入布局: + +``` +mPipeline->setVertexInputLayout(inputLayout); +``` + +由于我们目前还没有Uniform输入,因此可以创建一个空的着色器资源绑定: + +``` +QScopedPointer mShaderBindings; +``` + +```c++ +mShaderBindings.reset(mRhi->newShaderResourceBindings()); +mShaderBindings->create(); +mPipeline->setShaderResourceBindings(mShaderBindings.get()); +``` + +由于图像是直接绘制在交换链的 当前渲染目标 上,所以 重采样数 和 渲染通道描述 可以直接从交换链中 获取: + +``` c++ +mPipeline->setSampleCount(mSwapChain->sampleCount()); +mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); +``` + +GLSL代码的语法跟C语言非常相似,比较明显的区别就是: + +- 着色器代码中会定义各种 `in`,`out`,`uniform` 描述的变量 +- 着色器代码中只有一些[基础类型](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#variables-and-types),可以使用Block(类似struct),但需要注意内存布局和对齐。 +- 着色器代码中拥有很多 **GPU版本** 的[内置数学函数](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#built-in-functions) +- 各个阶段的着色器有它固定的[代码结构](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#built-in-variables)和[内置变量](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#built-in-variables) + +GLSL的基础结构并不复杂,就比如我们接下来要使用的代码,相信读懂它,对你来说很轻松: + +```c++ +QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; //这里需要与上面的inputLayout 对应 + layout(location = 1) in vec4 color; + + layout (location = 0) out vec4 vColor; //输出变量,这里的location是out的,而不是in + + out gl_PerVertex { //Vulkan GLSL中固定的定义 + vec4 gl_Position; + }; + + void main(){ + gl_Position = vec4(position,0.0f,1.0f); //根据输入的position,设置实际的顶点输出 + vColor = color; //将输入的color传递给fragment shader + } +)"); +Q_ASSERT(vs.isValid()); + +QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout (location = 0) in vec4 vColor; //上一阶段的out变成了这一阶段的in + layout (location = 0) out vec4 fragColor; //片段着色器输出,location 为 0 表示输出到 render target 的第一个颜色附件上 + void main(){ + fragColor = vColor; + } +)"); +Q_ASSERT(fs.isValid()); + +mPipeline->setShaderStages({ //将着色器安装到流水线上 + QRhiShaderStage(QRhiShaderStage::Vertex, vs), + QRhiShaderStage(QRhiShaderStage::Fragment, fs) +}); +``` + +最后,我们要做的就是,创建流水线: + +``` c++ +mPipeline->create(); +``` + +## 上传渲染数据 + +上面创建好了流水线和顶点缓冲区(VertexBuffer),现在我们需要将顶点数据上传到顶点缓冲区中,这里我们使用另一个信号 : + +``` c++ +QRhiEx::Signal mSigSubmit; //用于提交资源的信号 +``` + +在QRhi中, **QRhiResourceUpdateBatch** 可以用来合并 资源 的提交指令,我们可以通过下面的方式来创建它: + +``` c++ +QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); +``` + +它提供了以下操作: + +![image-20230501112954761](Resources/image-20230501112954761.png) + +由于我们的 VertexBuffer 是 `QRhiBuffer::Immutable` 类型(即静态不可变)的,所以可以这样来上传: + +```c++ +batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); //上传顶点数据 +``` + +之后,我们再将这些资源提交指令录制在 指令缓冲(Command Buffer)中: + +```C++ +cmdBuffer->resourceUpdate(batch); +``` + +结合上面的 渲染结构初始化,现在`onRenderTick`的代码看上去应该是: + +``` c++ +virtual void onRenderTick() override { + if(mSigInit.ensure()){ + // doing somethin + } + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); //交互链中的当前渲染目标 + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); //交互链中的当前指令缓冲 + + if (mSigSubmit.ensure()) { + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); //上传顶点数据 + cmdBuffer->resourceUpdate(batch); + } +} +``` + +## 录制渲染指令 + +在QRhi中,想对一个渲染目标(Render Target)进行渲染,需要开启一个渲染通道(Render Pass),并在渲染结束时关闭,就像是这样: + +```c++ +const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); //使用该值来清理 渲染目标 中的 颜色附件 +const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; //使用该值来清理 渲染目标 中的 深度和模板附件 +cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, nullptr); //开启一个渲染通道 + +/* +* 渲染逻辑 +*/ + +cmdBuffer->endPass(); //关闭渲染通道 +``` + +`beginPass` 和 `endPass` 的定义如下: + +``` c++ +void beginPass(QRhiRenderTarget *rt, + const QColor &colorClearValue, + const QRhiDepthStencilClearValue &depthStencilClearValue, + QRhiResourceUpdateBatch *resourceUpdates = nullptr, + BeginPassFlags flags = {}); + +void endPass(QRhiResourceUpdateBatch *resourceUpdates = nullptr) +``` + +可以看到`beginPass` 和 `endPass` 中也能使用 **QRhiResourceUpdateBatch** ,它与 `cmdBuffer->resourceUpdate(batch)` 作用一致,这里我们需要注意的是: + +- `nextResourceUpdateBatch()` 是从池中获取可操作的 **QRhiResourceUpdateBatch** 实例,而当前可操作实例仅有一个,这意味着:在调用`nextResourceUpdateBatch`之后,再调用一次`nextResourceUpdateBatch` 就会出错。除非我们使用 `QRhiCommandBuffer::resourceUpdate` , `beginPass` 或者 `endPass`,这些函数会处理 **QRhiResourceUpdateBatch** 实例,并destory它,让池中的下一个 **QRhiResourceUpdateBatch** 实例可以被正常使用。 +- 当开启一个渲染通道之后,就无法再上传渲染数据,这也就意味着我们需要在`beginPass` 和 `endPass`之外去提交渲染数据。 + +而执行渲染,主要是以下几个固定步骤: + +- 设置渲染管线(QRhiGraphicsPipeline) +- 设置视口(QRhiViewport) +- 设置描述符集布局绑定(QRhiShaderResourceBindings) +- 设置顶点输入(QRhiCommandBuffer::VertexInput) +- 调用draw函数 + +也就对应这样的代码: + +```C++ +//设置图形渲染管线 +cmdBuffer->setGraphicsPipeline(mPipeline.get()); + +//设置图像的绘制区域 +cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); +//设置描述符集布局绑定,如果不填参数(为nullptr),则会使用渲染管线创建时所使用的描述符集布局绑定 +cmdBuffer->setShaderResources(); + +//将 mVertexBuffer 绑定到 Binding 0 +const QRhiCommandBuffer::VertexInput vertexInput(mVertexBuffer.get(), 0); +//内存偏移值为0,只有一个VertexInput +cmdBuffer->setVertexInput(0, 1, &vertexInput); + +//执行绘制,其中 3 代表着有 3个顶点数据输入 +cmdBuffer->draw(3); +``` + +在创建图形渲染管线的时候,我们创建了顶点缓冲区和顶点输入布局,但并没有构建顶点缓冲区和图形渲染管线的连接,直到录制渲染指令的时候,才做了实际的绑定。 + +在绑定之后,流水线会从对应 Binding Index 的Buffer中,按之前定义好的布局去读取数据,就像是这样: + +![image-20230501131107153](Resources/image-20230501131107153.png) + +由于我们draw的参数为3,所以流水线只会读取前三个顶点数据,交由 顶点着色器 进行处理: + +![image-20230501131226503](Resources/image-20230501131226503.png) + +在几何装配阶段,会按不同的策略来挑选顶点,组装成一个基础图元(Point,Line,Triangle),流水线默认的策略是 `QRhiGraphicsPipeline::Topology::Triangles`,该策略会依次读取三个顶点数据组装成三角形: + +![image-20230501131250421](Resources/image-20230501131250421.png) + +再经由光栅化阶段后,几何图形上的每个片段(像素)都会被片段处理器进行处理: + +![image-20230501131443750](Resources/image-20230501131443750.png) + +### 图元拓扑(Primitive Topology) + +在上面创建流水线的代码中,我们并没有设置流水线的图元拓扑,如果想要设置,可以调用: + +``` c++ +mPipeline->setTopology(QRhiGraphicsPipeline::Topology::Triangles); +``` + +图元拓扑就决定了在几何装配阶段,流水线如何挑选顶点来组装基础图元,在QRhi中,支持以下几种拓扑策略: + +```c++ +enum Topology { + Triangles, + TriangleStrip, + TriangleFan, + Lines, + LineStrip, + Points, + Patches //用于镶嵌控制和评估着色器 +}; +``` + +QRhi默认使用的是 `Triangles`,假设现在有六个顶点(下方使用索引描述),不同拓扑对应的组装策略是: + +- **Triangles** :组装得到2个三角形`{0,1,2},{3,4,5}` +- **TriangleStrip** :组装得到4个三角形`{0,1,2},{2,1,3}{2,3,4},{4,3,5}`,顶点的异常索引顺序,是由于流水线会依据顶点的时钟顺序来判断三角形的正反面,所以在组装TriangleStrip的时候会确保时钟顺序不变。 +- **TriangleFan** :组装得到4个三角形`{0,1,2},{0,2,3},{0,3,4},{0,4,5}` +- **Lines** :组装得到3条线`{0,1},{2,3},{4,5}` +- **LineStrip** :组装得到5条线`{0,1},{1,2},{2,3},{3,4},{4,5}` +- **Points** :组装得到6个点`{0},{1},{2},{3},{4},{5}` + +这里有一个Vulkan中的图示: + +![img](Resources/905430d6d8ca6d241f51eab02af3be71232a0517.png@942w_683h_progressive.webp) + +## 大功告成 + +执行程序,如果可以看到下方图像,说明我们成功了!!! + +![image-20230501131634427](Resources/image-20230501131634427.png) + +可以此处找到完整的代码: + +- https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/02-GraphicRenderingPipeline/Source/main.cpp + +这里有一个很好的视频讲解了图形渲染管线基础: + +- [上帝视角看GPU(1):图形流水线基础](https://www.bilibili.com/video/BV1P44y1V7bu) + +此外,你还可以尝试一下: + +- 修改 图元 拓扑,绘制点,线。 +- 绘制矩形,圆形或者其他多边形图像。 +- 增加一些其他的顶点属性 + diff --git "a/Docs/01-GraphicsAPI/3.\347\235\200\350\211\262\345\231\250.md" "b/Docs/01-GraphicsAPI/3.\347\235\200\350\211\262\345\231\250.md" new file mode 100644 index 00000000..1775b9d1 --- /dev/null +++ "b/Docs/01-GraphicsAPI/3.\347\235\200\350\211\262\345\231\250.md" @@ -0,0 +1,622 @@ +--- +comments: true +--- + +# 着色器基础 + +上一节中,主要讲述了如何使用QRhi搭建基础的图形渲染管线,你或许没有发现,你其实已经能用GPU绘制任何图形了!要做的不过是填充顶点数据而已~ + +那按这样来说,后面的东西都不用学了 + +在现代图形API中,我们追求的不仅仅是把某些事情给做出来,还在于把能做的事情变得更多,把不可能的事情变得可能。 + +所以在图形API系列接下来的文章中,主要是围绕着如何使用符合硬件特性的操作来加速图形的绘制。 + +## 历史 + +GPU的出现是为了给当时的图形渲染体系提供一个可靠的硬件加速设备,所以在GPU发展的初期,图形API旨在提供一些简单通用的接口,将原先在CPU上的固定渲染流程转移到GPU上。 + +如果你接触过早期的OpenGL,对这样的代码应该并不陌生: + +```c++ +void init(void) +{ + glMatrixMode(GL_MODELVIEW); //设置模型视图矩阵为单位矩阵 + glLoadIdentity(); +} +void display(void) +{ + glClear(GL_COLOR_BUFFER_BIT); //清屏 + glBegin(GL_TRIANGLES); //开始写入 三角形图元 + glColor3f(1.0, 0.0, 0.0); glVertex3f(-1.0, -1.0, 0.0); //写入顶点数据0 + glColor3f(0.0, 1.0, 0.0); glVertex3f( 0.0, 1.0, 0.0); //写入顶点数据1 + glColor3f(0.0, 0.0, 1.0); glVertex3f( 1.0, -1.0, 0.0); //写入顶点数据2 + glEnd(); //结束 + glutSwapBuffers(); //刷新交换链 +} +void reshape(int w, int h) +{ + glViewport(0, 0, (GLsizei) w, (GLsizei) h); //调整视口大小 + glMatrixMode(GL_PROJECTION); //设置裁剪矩阵为单位矩阵 + glLoadIdentity(); +} +``` + +在这一阶段的图形API,渲染管线各个流程的处理逻辑都是固定的,开发者只需要向图形API提供顶点数据,纹理,矩阵,光源等信息,就能通过GPU加速完成一个3D场景的渲染。 + +虽然图形API允许通过少量参数和标识对进行图形渲染管线中的流程进行配置,但随着图形学的快速发展,各种渲染理论层出不穷,很显然,官方提供的接口已经跟不上学术界和工业界的发展,甚至还制约了开发者和研究人员的进一步探索,因此, **可编程的流水线 ([Programmable Pipeline](https://www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview))** 应运而生。 + +而现在,我们称旧时代的使用方式为 **固定功能的流水线([Fixed Function Pipeline](https://www.khronos.org/opengl/wiki/Fixed_Function_Pipeline))** 。 + +可编程管线允许开发者编写 **着色器 (Shader)** 来定制流水线某一阶段的处理逻辑。 + +而各个图形API都有自己的 **着色器语言(Shader Language)** ,比如: + +- OpenGL、Vulkan 使用 [GLSL](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/OpenGL_Shading_Language) +- Direct3D使用 [HLSL](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl) +- Metal使用 [MSL](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Metal_(API)) + +这些着色器语言的整体工作流程都差不多,比较大的区别就是符合自己API风格(标新立异)的语法。 + +这里有一个功能相同的片段着色器(Fragment Shader)在不同着色器语言下的实现: + +![image-20230503163338306](Resources/image-20230503163338306.png) + +早期的游戏开发商,往往会使用一种图形API来作为自己游戏引擎的渲染后端,但随着各个操作平台的用户数量逐渐上升,跨平台也吸引了各大厂商的注意,毕竟多支持一个平台,也就意味着多一个市场。 + +因此,现今大多的游戏引擎都会将这些图形API封装成统一的接口,可以在不同的平台上切换,来追求更好的图形性能,我们一般称这套接口为 **RHI** (Rendering Hardware Interface) + +而在封装过程中,除了要注意图形API的指令和资源,跨平台的着色器也是一个非常头疼的问题。 + +目前,大多数引擎都会在一种着色器语言下进行开发,再转译成其他图形API可用的着色器语言,比如: + +- UE使用 HLSL 进行开发 ,借助开源库 [Glslang](https://github.com/KhronosGroup/glslang) 和 [Spirv-Cross](https://github.com/KhronosGroup/SPIRV-Cross) 来实现跨平台的着色器转译 +- Unity,O3DE,Godot,Bgfx等引擎自定义了一种新的着色器语言来进行开发,通过内置编译器再搭配开源库来实现跨平台的着色器转译 + +这里有一些相关的阅读: + +- [[知乎-unwind] 跨平台引擎Shader编译流程分析](https://zhuanlan.zhihu.com/p/56510874) + +- [[知乎-网页游戏雷火事业群] 游戏着色器(Shader)基础介绍](https://zhuanlan.zhihu.com/p/599456569) + +## 着色器转译 + +QRhi的跨平台着色器方案跟UE相似,只不过QRhi使用Vulkan风格的GLSL开发而非HLSL,它的转译流程如下: + +![img](Resources/v2-b8badd685b39f310d5420e95c1283995_1440w.webp) + +在上一节中,我们使用了这样的代码来创建着色器: + +``` c++ +QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; //这里需要与上面的inputLayout 对应 + layout(location = 1) in vec4 color; + + layout (location = 0) out vec4 vColor; //输出变量,这里的location是out的,而不是in + + out gl_PerVertex { //Vulkan GLSL中固定的定义 + vec4 gl_Position; + }; + + void main(){ + gl_Position = vec4(position,0.0f,1.0f); //根据输入的position,设置实际的顶点输出 + vColor = color; //将输入的color传递给fragment shader + } +)"); +Q_ASSERT(vs.isValid()); + +QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout (location = 0) in vec4 vColor; //上一阶段的out变成了这一阶段的in + layout (location = 0) out vec4 fragColor; //片段着色器输出,location 为 0 表示输出到 render target 的第一个颜色附件上 + void main(){ + fragColor = vColor; + } +)"); +Q_ASSERT(fs.isValid()); +``` + +其中`newShaderFromCode()`并非是QRhi提供的原生函数,而是笔者调用 **QtShaderTools** 模块中接口进行了一些简单封装,它的真实逻辑如下: + +``` c++ +#include "private/qshaderbaker_p.h" + +QShader newShaderFromCode(QShader::Stage stage, const char* code) { + QShaderBaker baker; //着色器烘培器 + baker.setGeneratedShaderVariants({ QShader::StandardShader }); + baker.setGeneratedShaders({ + QShaderBaker::GeneratedShader{QShader::Source::SpirvShader,QShaderVersion(100)}, + QShaderBaker::GeneratedShader{QShader::Source::GlslShader,QShaderVersion(430)}, + QShaderBaker::GeneratedShader{QShader::Source::MslShader,QShaderVersion(12)}, + QShaderBaker::GeneratedShader{QShader::Source::HlslShader,QShaderVersion(60)}, + }); + baker.setSourceString(code, stage); //装配GLSL源码 + QShader shader = baker.bake(); //执行烘培,将之转译成各个图形API的着色器代码 + + if (!shader.isValid()) { //打印编译报错 + QStringList codelist = QString(code).split('\n'); + for (int i = 0; i < codelist.size(); i++) { + qWarning() << i + 1 << codelist[i].toLocal8Bit().data(); + } + qWarning(baker.errorMessage().toLocal8Bit()); + } + return shader; +} +``` + +QShaderBaker 来自于 **Qt Shader Tools** 的私有模块,在使用CMake构建的时候,需要为自己的构建目标链接`Qt::ShaderToolsPrivate`,使用它可以将Vulkan风格的GLSL编译成一个QShader对象(里面包含了各个版本图形API的着色器代码),这是一种在 **运行时转译** 着色器代码的方法。 + +此外,我们还可以利用Qt提供的 **qsb** 命令行工具 **离线转译** 着色器代码,该工具位于如下的目录中: + +![image-20230503153613484](Resources/image-20230503153613484.png) + +> 你可以给该目录添加到系统环境变量中,这样就能在全局访问qsb工具 + +在窗口地址栏中输入 `cmd`, 按下回车,可以打开命令行窗口,在其中输入 `qsb.exe -h` 可以看到该命令行工具的使用说明: + +![image-20230503153838795](Resources/image-20230503153838795.png) + +比如使用指令`qsb.exe -c color.frag -o color.frag.qsb --glsl 430 --msl 12 --hlsl 60 ` 可以将当前目录的`color.frag`文件转译生成一个 `color.frag.qsb`文件,该文件包含了 430版本的GLSL,12版本的MSL,60版本的HLSL。 + +在Qt中,可以通过如下方式来使用`*.qsb`文件: + +```c++ +QShader newShaderFromQSBFile(const char* filename) { + QFile file(filename); + if (file.open(QIODevice::ReadOnly)) + return QShader::fromSerialized(file.readAll()); + return QShader(); +} +``` + +在CMake中,还提供了一个有用的功能:当某些文件发生变动时,执行一些操作。 + +通过这个功能,我们还可以实现 **编译时** 的着色器转译。 + +该功能通过CMake指令 [add_custom_command](https://cmake.org/cmake/help/latest/command/add_custom_command.html) 来实现,我们可以在CMake中增加以下函数来添加Shader + +```cmake +function(add_shader TARGET_NAME SHADER_PATH) + set(OUTPUT_SHADER_PATH ${SHADER_PATH}.qsb) #输出文件路径 + add_custom_command( + OUTPUT ${OUTPUT_SHADER_PATH} #指定输出文件 + COMMAND qsb.exe -c ${SHADER_PATH} -o ${OUTPUT_SHADER_PATH} --glsl 430 --msl 12 --hlsl 60 #执行QSB工具 + MAIN_DEPENDENCY ${SHADER_PATH} #指定依赖文件,即该文件变动时,触发上述命令 + ) + set_property(TARGET ${TARGET_NAME} APPEND PROPERTY SOURCES ${OUTPUT_SHADER_PATH}) #需要把输出文件添加到一个构建目标中,才会触发CustomCommand + source_group("Shader Files" FILES ${SHADER_PATH} ${OUTPUT_SHADER_PATH}) #将着色器文件分类 +endfunction() +``` + +这样的话,在构建每次`TARGET_NAME` 时,CMake都会检测`SHADER_PATH`有没有发生改变,如果发生改变,则会调用自定义指令,这里也就是调用`QSB`工具生成`*.qsb`文件,报错信息也会出现在IDE的错误面板上: + +![image-20230503162819180](Resources/image-20230503162819180.png) + +关于Qt的着色器工具,可以在这里发现一些测试示例: + +- https://github.com/qt/qtshadertools/blob/dev/tests/auto/qshaderbaker/tst_qshaderbaker.cpp + +在该教程仓库中有一个示例演示了QRhi中编译着色器的各种方式: + +- https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/03-Shader/Source/main.cpp + +## 着色器编写 + +### 基本类型 + +GLSL中一些常见的基础类型如下: + +| Type | Meaning | +| :--------- | :------------------------------------------------------- | +| **void** | for functions that do not return a value | +| **bool** | a conditional type, taking on values of true or false | +| **int** | a signed integer | +| **uint** | an unsigned integer | +| **float** | a single-precision floating-point scalar | +| **double** | a double-precision floating-point scalar | +| **vec2** | a two-component single-precision floating-point vector | +| **vec3** | a three-component single-precision floating-point vector | +| **vec4** | a four-component single-precision floating-point vector | +| **mat2** | a 2 × 2 single-precision floating-point matrix | +| **mat3** | a 3 × 3 single-precision floating-point matrix | +| **mat4** | a 4 × 4 single-precision floating-point matrix | +| **sampler1D** **texture1D** **image1D** | a handle for accessing a 1D texture | +| **sampler1DArray** **texture1DArray** **image1DArray** | a handle for accessing a 1D array texture | +| **sampler2D** **texture2D** **image2D** | a handle for accessing a 2D texture | +| **sampler2DArray** **texture2DArray** **image2DArray** | a handle for accessing a 2D array texture | +| **sampler2DRect** **texture2DRect** **image2DRect** | a handle for accessing a rectangle texture | +| **sampler3D** **texture3D** **image3D** | a handle for accessing a 3D texture | +| **samplerCube** **textureCube** **imageCube** | a handle for accessing a cube mapped texture | +| **samplerCubeArray** **textureCubeArray** **imageCubeArray** | a handle for accessing a cube map array texture | +| **samplerBuffer** **textureBuffer** **imageBuffer** | a handle for accessing a buffer texture | + +完整的类型名单可以参阅: + +- https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.html#basic-types + +其中主要需要注意的是 **vec(向量** )和 **mat(矩阵)** ,这里我们并不讨论它们的几何意义,如果对这些不熟,笔者推荐可以看一下《3D数学基础:图形和游戏开发》 + +#### 向量 + +在数学中,向量(也称为欧几里得向量、几何向量、矢量),指具有大小(magnitude)和方向的量。而在计算机中,向量只是几个连续的数字。GLSL支持二维(vec2),三维(vec3),四维(vec4)向量。 + +关于向量的一些基础使用方式,以vec4为例: + +``` c++ +vec4 mVector = vec4(1,2,3,4); +float x = mVector.x; //1 +vec2 xy = mVector.xy; //[1,2] +vec3 xzy = mVector.xzy; //[1,3,2] +vec2 xxw = mVector.xxw; //[1,1,4] +vec4 newVecotr = vec4(mVector.xx,-1,-2); //[1,1,-1,-2] +``` + +这里我们使用`xyzw`访问向量的分量,GLSL还支持使用其他几组关键字`rgba`和`pqst`,使用哪组关键字取决于使用场景以及个人喜好。 + +GLSL当然也支持向量的加,减,乘,除,点乘等。 + +#### 矩阵 + +在数学中,矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合。在计算机中,它同样只是一堆连续的数字。GLSL支持的最大矩阵尺寸为4x4,也就是mat4x4(这里用字母x代指乘号),因为是方阵所以可以简写为mat4 + +需要特别注意的是:GLSL使用的矩阵是按列存储的 + +``` c++ +mat3 mMatrix = mat4(0,1,2, + 3,4,5, + 6,7,8); +``` + +如果换成按行存储,它的表示就变成了: + +``` c++ +{ + 0,3,6, + 1,4,7, + 2,5,8 +} +``` + +存储方式的不同不仅仅影响矩阵的访问方式,更为重要的是它将影响矩阵的运算顺序,下图描述了不同存储方式下,三维矩阵与三维向量相乘的结果是什么类型: + +![img](Resources/96997598049549ea6f3691dd3f71d92b0c4646dd.png@942w_711h_progressive.webp) + +我们在GLSL中使用矩阵一般是对向量进行变换,因此我们运算是一般是从右往左进行计算。如下所示: + +``` c++ +vec3 pos = vec3(0.5,1.0,0); +mat3 model = mat3(...); +vec3 newPos = mat * pos; +``` + +另外,我们还需要注意的是,除了矩阵和向量的运算要反过来,矩阵的一些内部运算也得反着来。比如我们在客户端中使用QMatrix4x4想对一个图形先进行旋转,再进行平移,应该这么来使用: + +```c++ +QMatrix4x4 mat4; +mat4.translate(0,1); //先执行的操作后调用 +mat4.rotate(60,QVector3D(0,0,1)); //所以这里是先旋转后平移 +``` + +> 还需要注意的是 QMatrix4x4 的内存大小并不是 `16 * sizeof(float)`,它还带有一个四字节的标识位,当需要将QMatrix4x4上传到GPU时,请调用`mat.toGenericMatrix<4,4>()`转换为 **QGenericMatrix** 类型 + +### 限定符 + +GLSL中,我们会使用一些限定符来描述变量的一些相关信息,常见的限定符类型有: + +- 存储限定符:描述变量的存储位置 +- 布局限定符:描述变量的布局 + +常见的存储限定符有: + +| Storage Qualifier | Meaning | +| :---------------- | :----------------------------------------- | +| 无 | 可读写的本地变量 | +| **const** | 常量 | +| **in** | 说明该变量为输入变量,来自于前一阶段的输出 | +| **out** | 说明该变量为输出变量,将作为下一阶段的输入 | +| **uniform** | 说明该变量为Uniform数据 | + +需要注意的就是`in`,`out`和`uniform`之间的区别: + +![image-20230503191610995](Resources/image-20230503191610995.png) + +布局限定符的格式为: + +- `layout( layout-qualifier-id-list )` + +其中`layout-qualifier-id-list` 是一个特征数组,它的元素可以是一个标识,也可以是一个键值对(Key-Value),其他常见的布局限定符特征有: + +| 布局限定符特征 | 描述 | +| :----------------------------------------------------------- | :----------------------------------------------------------- | +| **binding** = | 用于对应uniform数据在 描述符集绑定布局 中的index | +| **location** = | 用于确定 所有 输入 输出变量 的 位置,着色器前一阶段的输入变量和后一阶段的输出变量是通过location来确定连接的,而不是变量名 | +| **shared** , **packed** , **std140** , **std430** | 用于声明 Block 所采用的结构体内存对齐方式 | +| **local_size_x** = , **local_size_y** = , **local_size_z** = | 在计算着色器中,说明计算单元的大小 | +| **max_vertices** = | 在几何着色器中,定义最大的顶点数量 | +| **rgba32f** **rgba16f** **rg32f** **rg16f** **r11f_g11f_b10f** **r32f** **r16f** **rgba16** **rgb10_a2** **rgba8** **rg16** **rg8** **r16** **r8** **rgba16_snorm** **rgba8_snorm** **rg16_snorm** **rg8_snorm** **r16_snorm** **r8_snorm** **rgba32i** **rgba16i** **rgba8i** **rg32i** **rg16i** **rg8i** **r32i** **r16i** **r8i** **rgba32ui** **rgba16ui** **rgb10_a2ui** **rgba8ui** **rg32ui** **rg16ui** **rg8ui** **r32ui** **r16ui** **r8ui** | 在计算着色器中,说明Image变量的图像格式 | + +在Vulkan风格的GLSL中,我们需要注意的是: + +- 任意的`in`或`out`变量都需要使用 `layout(location = ${index})` 去修饰,因为这一阶段的`in变量`会根据`Location Index` 去找上一阶段对应的`out变量` + - 对于顶点着色器,它的in变量会根据`Location Index`是去从流水线的顶点输入布局去找 + - 对于片段着色器,它的out变量会`Location Index` 输出到渲染目标对应索引的颜色附件中 +- 任意的 `uniform` 变量都需要使用 `layout(binding = ${index})`去修饰,`Binding Index`需要与流水线中所使用的描述符集布局(QRhiShaderResourceBindings)对应 + +限定符完整的文档,请查阅: + +- https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#storage-qualifiers + +### 基础阶段 + +一个完整的图形渲染管线执行流程如下: + +![img](Resources/GL-Pipeline.jpg) + +- 红色部分的 **Vertex Shader** 和 **Fragment Shader** 是图形渲染管线中的 **必填项** + +- 黄色部分的 **Tesselation Control Shader** , **Tesselation Evaluation Shader** 和 **Geometry Shader** 是图形渲染管线中的 **可填项** + +- 绿色部分的 **Primitive Setup** , **Clipping** 和 **Rasterization** 是 **不可编程阶段** ,但图形API提供了一些参数可以调整它们的行为 + +想要编写着色器,我们首先要了解图形渲染管线中各个阶段的工作职责,这里笔者主要对顶点着色器和片段着色器进行说明,图形API中还有其他的处理阶段和流水线,提前展开会给读者带来大量的认知负担,如果觉得感兴趣,可以先尝试了解,这些内容也会在后续的文章进一步深入。 + +#### 顶点着色器(Vertex Shader) + +**顶点着色器 (Vertex Shader )** 的职责在于:根据 `输入的顶点数据` 和 `Uniform数据(如果有)` ,来填写当前顶点 **内置的输出变量** ,并将一些有用数据传递给下一阶段。 + +一个核心的顶点着色器模板如下: + +``` c++ +layout(location = 0) in vec2 position; + +out gl_PerVertex { + vec4 gl_Position; +}; + +void main(){ + gl_Position = vec4(position,0.0f,1.0f); +} +``` + +解读: + +- `layout(location = 0)` 代表了值对应流水线创建时顶点输入布局中 索引为0 的顶点属性 +- `in` 代表是输入变量 +- `vec2`表示该变量类型是二维向量,它需要与顶点输入布局中的格式(Float2)对应 +- `position`是输入变量的名称,它可以是任意的 +- `out gl_PerVertex { vec4 gl_Position; };` 是顶点着色器中固定输出定义 +- `gl_Position = vec4(position,0.0f,1.0f);`就是根据输入的顶点位置,填充顶点着色器实际的顶点数据输出 + +需要注意的是:千万不用受定式思维的影响, 以为这里必须要输入一个position,实际上,我们可以输入任意的顶点数据,甚至不包括顶点位置,只需要在顶点着色器中,确定`gl_PerVertex`中`gl_Position`的值即可,比如可以是这样: + +```c++ +layout(location = 0) in vec2 anyVertexData; + +out gl_PerVertex { + vec4 gl_Position; +}; + +vec1 calcPosition(vec2 vertexData){ + // do something + return position; +} + +void main(){ + gl_Position = calcPosition(anyVertexData) +} +``` + +如果我们想要让该阶段的一些数据传递到下一阶段,那么只需要定义 `out` 修饰且带有`布局(layout)描述`的变量: + +```c++ +layout(location = 0) in vec2 position; +layout(location = 0) out vec4 color; //定义输出变量,in和out的location是分离的 + +out gl_PerVertex { + vec4 gl_Position; +}; + +void main(){ + gl_Position = vec4(position,0.0f,1.0f); + color = vec4(position.xy,0.0f,1.0f); +} +``` + +如果流水线中有顶点着色器的 Uniform数据 ,只需要定义用`uniform`修饰且带有`绑定(binding)描述`的变量: + +```c++ +layout(location = 0) in vec2 position; +layout(location = 0) out vec4 color; + +layout(binding = 0) uniform UniformBlock { //这里的binding=0对应流水线创建时,描述符集布局绑定的设置 + vec4 color; //UniformBlock是该数据结构块的名称,它可以是任意的 +} ubo; + + +out gl_PerVertex { + vec4 gl_Position; +}; + +void main(){ + gl_Position = vec4(position,0.0f,1.0f); + color = ubo.Color; // 通过Uniform数据设置颜色 +} +``` + +#### 片段着色器(Fragment Shader) + +**顶点着色器 (Vertex Shader )** 的职责在于:根据 `前一阶段的输入数据` 和 `Uniform数据(如果有)` ,写入到渲染目标(Render Target)的`颜色附件`上。 + +一个核心的片段着色器模板如下: + +``` c++ +layout(location = 0) out vec4 fragColor; +void main(){ + fragColor = vec4(1,1,1,1); +} +``` + +解读: + +- `layout(location = 0) out` 表明该片段将输出到 渲染目标 `索引为0的颜色附件` 上,一个渲染目标往往至少包含一个颜色附件,交换链的渲染目标上就只有一个四通道的颜色附件 。 +- `vec4`代表着颜色附件具有四个颜色通道,例如`RGBA8888` +- `fragColor` 是输出变量的名称,它可以是任意的 + +- `fragColor = vec4(1,1,1,1)`说明了最终的片段为不透明的白色(R=1,G=1,B=1,A=1) + +片段着色器也能像顶点着色器那样增加一些Uniform输入。 + +在笔者学习的初期,因为顶点着色器的输出变量和片段着色器中的输入变量是一一对应的,就把顶点着色器和顶点着色器也当成是一一对应的,但实际上这是错误的,还记得上一节的程序吗? + +我们通过 `3` 个顶点数据: + +```c++ +static float VertexData[] = { //顶点数据 + //position(xy) color(rgba) + 0.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, + -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, +}; +``` + +绘制出了这样的图像: + +![image-20230503194533809](Resources/image-20230503194533809.png) + +仔细查看,明明只输入3个顶点的颜色,但最终的图像中有多少种颜色? + +很显然,不是3种,那这是怎么做到呢? + +通过下图,我们很容易发现,顶点着色器和片段着色器并不是一一对应的,顶点在被装配成几何图元之后,经由光栅化阶段,会生成很多片段: + +![image-20230503194825627](Resources/image-20230503194825627.png) + +而这个过程在着色器层面可以看做是:顶点着色器的输出变量,在经过光栅化之后,变成了很多片段的输入变量。 + +而输出变量到输入变量之间多对多的转换,是通过 **光栅化插值** 完成的,关于它的细节,请查阅: + +- [[知乎-木头骨头石头] 图形渲染基础:光栅化算法](https://zhuanlan.zhihu.com/p/370059588) + +### 内置变量 + +在流水线中,往往还会提供一些内置变量,用于给着色器程序传递一些上下文信息。 + +比如在顶点着色器中: + +``` c++ +in int gl_VertexIndex; // 当前顶点的索引 +in int gl_InstanceIndex; // 当前实例的索引 +in int gl_DrawID; // Requires GLSL 4.60 or ARB_shader_draw_parameters +in int gl_BaseVertex; // Requires GLSL 4.60 or ARB_shader_draw_parameters +in int gl_BaseInstance; // Requires GLSL 4.60 or ARB_shader_draw_parameters + +out gl_PerVertex { + vec4 gl_Position; // gl_Position是必要的,可以了解一下其他变量的作用 + float gl_PointSize; + float gl_ClipDistance[]; + float gl_CullDistance[]; +}; + +``` + +片段着色器中也有: + +``` c++ +in vec4 gl_FragCoord; //当前片段的坐标 +in bool gl_FrontFacing; //当前片段是否是正面 +in float gl_ClipDistance[]; +in float gl_CullDistance[]; +in vec2 gl_PointCoord; +in int gl_PrimitiveID; +in int gl_SampleID; +in vec2 gl_SamplePosition; +in int gl_SampleMaskIn[]; +in int gl_Layer; +in int gl_ViewportIndex; +in bool gl_HelperInvocation; + +out float gl_FragDepth; +out int gl_SampleMask[]; +``` + +关于内置变量,详见: + +- https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#built-in-language-variables + +### 内置函数 + +GLSL基本上内置了所有在图形开发中所使用的函数,比如: + +数学函数,向量运算(点乘,叉乘),矩阵转置,求逆,混合函数,插值函数等等的,基本你能想到的,需要的,基本都会有。 + +关于这些函数使用细节,请查阅下面的官方文档: + +- https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.4.60.html#built-in-functions + +### 逻辑分支 + +着色器语言相对于开发者来说,不能将简单地将它看做是一个不同语法的编程语言,它最特殊点就在于它是在GPU上执行的,它的内置函数实现都是基于 GPU 硬件特性的。 + +由于GPU是通过大量计算单元堆叠而成,从而也造就了它对逻辑的执行并不友好的缺点。 + +在编写GLSL的时候,我们需要尽可能地避免在GLSL中出现 **分支指令** ,而是从另一种策略去思考问题: + +- 首先可以明确:在着色器中调用`return`,并不会影响后续的阶段,如果必要的参数没有填写,则可能会使用默认值 +- 我们想要实现逻辑分支的主要目标,一般是为了让一部分情况输出A,另一个部分情况输出B,其他情况输出... + +GPU是对数学运算友好的,如果我们能确定 逻辑条件 到 输出结果 的函数式,通过数值计算的方式,就能避开逻辑分支,这里涉及到了一个关键函数: + +- `step` (阶跃)函数 + +一般情况下,数学函数的图像都是连续平缓的,阶跃函数特殊的点就在于,它存在值的跳变,它的函数图像是这样的: + +![image-20230503201624943](Resources/image-20230503201624943.png) + +![image-20230503201733475](Resources/image-20230503201733475.png) + +这个跳变的特征非常适用于数值上的逻辑分支,但它的使用却不太符合人的直觉,所以在GLSL中,允许我们使用条件运算符来完成相关的逻辑,就比如:`(a < b) ? a : b` + +GLSL中的一些内置函数,比如min,max等,它是特定于GPU的实现,所以我们无需关心逻辑分支的问题。我们只需要注意`if`的使用,而`if` 并不等价于分支,真正重要的是编译器是否生成了 **分支指令** 。 + +关于这些问题细节和注意事项,这里有一个非常好的说明: + +- [[知乎-YAO] Shader中的 if 和分支](https://zhuanlan.zhihu.com/p/122467342) + +## 流水线缓存(Pipeline Cache) + +上文我们提到,游戏引擎中,通常会在一种着色器语言下开发,再转译成各个图形API的着色器代码,这种转译我们一般得到的是具体图形API的`源代码` 或者 `字节码(中间)文件`。 + +在GPU使用着色器的时候,其实还需要将`源代码` 或者 `字节码(中间)文件`编译成机器可执行的二进制码,而这个编译过程是依赖于图形驱动的,而由于每个用户的图形驱动版本可能是不同的,开发商很难覆盖所有的图形驱动,这也就意味着我们必须在用户的电脑上去编译着色器的二进制码。 + +为了避免在程序执行过程中,着色器编译导致出现卡顿,现代图形API都提供了流水线缓存(Pipeline Cache)的功能,这个功能可以让程序在首次启动时,编译完所有的流水线,并将它存储到磁盘上,这样程序在运行时或者下次启动时就不会因为编译着色器出现卡顿。 + +在Unreal Engine中,称这个技术为 [PSO缓存(Pipeline State Object Cache)](https://docs.unrealengine.com/5.1/zh-CN/optimizing-rendering-with-pso-caches-in-unreal-engine/) + +这里有一个很好的文章说明了PSO缓存相关知识: + +- [[知乎-mike] UE4 PSO缓存](https://zhuanlan.zhihu.com/p/572503905) + +在QRhi中,我们可以在创建QRhi的时候,开启流水线缓存的功能: + +![image-20230503205925995](Resources/image-20230503205925995.png) + +使用函数 `QByteArray QRhi::pipelineCacheData()`可以获取到当前程序已经编译好的流水线缓存数据 + +使用函数 `void QRhi::setPipelineCacheData(const QByteArray &data)`可以设置流水线缓存数据 + +在程序调用`QRhiGraphicsPipeline::create()`时,会根据当前的流水线参数状态,去查询是否存在流式线状态,如果有,则直接复用,而不会重新编译。 + +## 学习平台 + +外网有两个非常有意思的平台,它们通过固定流程的流水线,在仅提供一些外部输入变量和修改片段着色器代码的前提下,就能制作出很多非常炫酷的图形效果: + +- Shadertoy:https://www.shadertoy.com/ + +![image-20230503214715725](Resources/image-20230503214715725.png) + +- Glsl Sandbox:http://glslsandbox.com/ + +![image-20230503214831931](Resources/image-20230503214831931.png) + +这两个平台都搭配了完善的在线Shader编辑器,你可以在上面尝试模仿一些简单的效果,并熟悉着色器的基本语法。 + +祝你好运! diff --git "a/Docs/01-GraphicsAPI/4.\347\274\223\345\206\262\345\214\272\344\270\216\347\272\271\347\220\206.md" "b/Docs/01-GraphicsAPI/4.\347\274\223\345\206\262\345\214\272\344\270\216\347\272\271\347\220\206.md" new file mode 100644 index 00000000..425348f9 --- /dev/null +++ "b/Docs/01-GraphicsAPI/4.\347\274\223\345\206\262\345\214\272\344\270\216\347\272\271\347\220\206.md" @@ -0,0 +1,579 @@ +--- +comments: true +--- + +# 缓冲区与纹理 + +## 缓冲区 + +**缓冲区(Buffer)** 持有一段GPU上的内存(Memory)以及一些相关特征—— **类型(Type)** 和 **用途(Usage)** + +**类型(Type)** 决定的Buffer的存储特性,图形API一般将其划分为: + +- **Host Local Memory** :只对Host可见的内存,通常称之为普通内存 +- **Device Local Memory** :只对Device可见的内存,通常称之为显存 +- **Host Local Device Memory** :由Host管理的,对Device看见的内存 +- **Device Local Host Memory** :由Device管理的,对Host可见的内存 + +> Host 往往是 CPU端, Device 指 GPU 端 + +关于细节,请查阅: + +- [Vulkan 内存管理](https://zhuanlan.zhihu.com/p/166387973) + +使用合适的内存类型能大幅提升内存的读写效率,在现代图形引擎中的流水线中,会尽可能地使用 **Device Memory** ,当我们要从CPU中提交数据给它时,由于Host无法访问,一般会将数据上传到一个 **Host可见的Memroy** 上,再通过指令拷贝到对应的 **Device Memory** 上,我们一般称这个持有主机可见内存的Buffer为 [Staging Buffer](https://link.zhihu.com/?target=https%3A//vulkan-tutorial.com/Vertex_buffers/Staging_buffer) + +在 **用途(Usage)** 上,Buffer 通常可具备以下 **标识(Flag)** : + +- **VertexBuffer** :用于存放顶点数据 +- **IndexBuffer** :用于存放索引数据 +- **UniformBuffer** :用于存储常量数据 +- **StorageBuffer** :用于Compute管线中的数据计算 +- **IndirectDrawBuffer** :用于间接渲染提供渲染参数 + +在图形渲染管线章节中,我们使用QRhi很轻松地创建了一个用于存储顶点数据的缓冲区( **VertexBuffer** ): + +``` c++ +QScopedPointer mVertexBuffer; +mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); +mVertexBuffer->create(); //在QRhi中,调用渲染资源对象的create函数才实际创建对应的GPU资源,在这之前,我们都是调整参数状态机而已 +``` + +使用函数`newBuffer`就能申请一个新的缓冲区: + +```c++ +QRhiBuffer* QRhi::newBuffer(QRhiBuffer::Type type, + QRhiBuffer::UsageFlags usage, + quint32 size); +``` + +函数参数所代表的意义如下: + +- **Type** :Buffer的类型,决定了Buffer的存储特性。 + + - **Immutable** :用于存放希望永远不会发生改变的数据,具有非常高效的GPU读写性能,它通常放置在 **Devices Local** 的 GPU 内存上,无法被CPU直接读写,但QRhi却支持它的上传,其原理是:每次上传数据新建一个 **Host Local** 的 **Staging Buffer** 作为中转来上传新数据,这样操作的代价是非常高昂的。 + + - **Static** :同样存储在 **Devices Local** 的 GPU 内存上,与 Immutable 不同的是,首次上传数据创建的 **Staging Buffer** 会一直保留。 + + - **Dynamic** :用于存放频繁发生变化的数据,它放置 **Host Local** 的GPU内存中,为了不拖延图形渲染管线,它通常会使用双缓冲机制。 + +- **Usage** :Buffer的用途,`Flags`说明是可以使用运算符`|`让多个`Flag`共存 + + - **VertexBuffer** :表明该Buffer可作为顶点缓冲区,存储顶点数据,作为图形渲染管线的输入 + - **IndexBuffer** :表明该Buffer可作为索引缓冲区,存储索引数据,用于挑选顶点数据 + - **UniformBuffer** :表明该Buffer可作为Uniform缓冲区,存储Uniform数据,作为着色器的公共输入 + - **StorageBuffer** :表明该Buffer可作为Storage缓冲区,该缓冲区可被计算着色器读写 + +- **Size** :Buffer的事情大小(单位为字节) + +### 顶点缓冲区 + +**顶点缓冲区(VertexBuffer)** 用作流水线的输入 + +在QRhi中,体现在录制渲染指令时必须为图形渲染管线指定顶点输入: + +```c++ +cmdBuffer->setGraphicsPipeline(mPipeline.get()); +cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); +cmdBuffer->setShaderResources(); +const QRhiCommandBuffer::VertexInput vertexInput(mVertexBuffer.get(), 0); //将 mVertexBuffer 绑定到Buffer0,内存偏移值为0 +cmdBuffer->setVertexInput(0, 1, &vertexInput); +cmdBuffer->draw(3); +``` + +而在创建流水线的时候,必须为流水线设置 **顶点输入布局(VertexInputLayout)** ,它用于制定缓冲区的解析规则,在之前的章节中,我们使用了这样的顶点数据: + +``` c++ +static float VertexData[] = { //顶点数据 + //position(xy) color(rgba) + 0.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, + -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, +}; +``` + +为了让顶点数据能够被下面的 顶点着色器输入 使用: + +``` glsl +layout(location = 0) in vec2 position; +layout(location = 1) in vec4 color; +``` + +我们可以定义这样的顶点输入布局: + +``` c++ +QRhiVertexInputLayout inputLayout; +inputLayout.setBindings({ //定义每个VertexBuffer单组顶点数据的跨度,这里只使用了一个VertexBuffer + //这里是6*sizeof(float),可以当作是GPU会从索引为0的Buffer中读取24字节数据作为单组顶点数据传给VertexShader + QRhiVertexInputBinding(6 * sizeof(float)) +}); + +inputLayout.setAttributes({ + //在从Buffer 0得到的单组顶点数据中,以偏移值为0,读取Float2大小的数据作为 location 0 的输入 + QRhiVertexInputAttribute(0, 0 , QRhiVertexInputAttribute::Float2, 0), + + //在从Buffer 0得到的单组顶点数据中,以偏移值为sizeof(float)*2,读取Float4大小的数据作为 location 1 的输入 + QRhiVertexInputAttribute(0, 1 , QRhiVertexInputAttribute::Float4, sizeof(float) * 2), +}); +``` + +在图形渲染的处理过程中,计算机关注的只是内存上的数据,并不在意这些数据的象征意义,正因为顶点输入布局的存在,使得我们可以编程时自行组织顶点的存储结构。 + +上面我们将所有的数据都放在了一个`float数组`中,现在我们可以使用一个更直观的结构: + +``` c++ +struct Vertex{ //顶点的结构 + QVector2D position; + QVector4D color; +}; +QVector vertices = { //顶点数据 + { { 0.0f, -0.5f}, {1.0f, 0.0f, 0.0f, 1.0f }}, + { {-0.5f, 0.5f}, {0.0f, 1.0f, 0.0f, 1.0f }}, + { { 0.5f, 0.5f}, {0.0f, 0.0f, 1.0f, 1.0f }}, +}; +``` + +我们只需填充顶点数据,使用`sizeof(Vertex) * vertices.size()`创建顶点缓冲区: + +```c++ +QScopedPointer mVertexBuffer; + +mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(Vertex) * vertices.size())); +mVertexBuffer->create(); +``` + +并使用这样的顶点输入布局: + +```c++ +QRhiVertexInputLayout inputLayout; +inputLayout.setBindings({ + QRhiVertexInputBinding(sizeof(Vertex)) //单个顶点数据的跨度也就是Vertex结构的大小 +}); + +inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0 , QRhiVertexInputAttribute::Float2, offsetof(Vertex,position)), + QRhiVertexInputAttribute(0, 1 , QRhiVertexInputAttribute::Float4, offsetof(Vertex,color)), +}); +``` + +> **offsetof** 运算符可用于获取成员变量在结构体或类中的内存偏移 + +上传时使用数组的裸数据即可: + +```c++ +batch->uploadStaticBuffer(mVertexBuffer.get(), mVertices.data()); +``` + +### 索引缓冲区 + +**索引缓冲区(IndexBuffer)** 用于挑选输入到流水线中的顶点数据 + +一个通用的应用场景是: + +- 由于图形API支持的基础图元只有点,线和三角形,在绘制多边形的时候,会使用多个三角形进行拼接,以四边形为例,一个四边形可划分为两个三角形,而两个三角形有六个顶点,但实际上四边形只有四个顶点,如果使用三角形拼接,会需要浪费两个顶点数据的空间大小来存储重叠的顶点,所有我们需要一种策略,来复用顶点缓冲区中的顶点数据,以完成 使用四个顶点就能绘制出由两个三角形组成的四边形。 + +流水线处理顶点输入时,会根据顶点输入布局,将顶点缓冲区中的数据划分成一组组有序的顶点数据,在 **默认情况** 下,会将这些顶点数据都交给VertexShader进行处理。 + +而索引缓冲区的作用,就是在划分成一组组有序的顶点数据之后,通过一系列索引(Index),在原先的顶点数据上进行挑选,组装出新的顶点数据交给VertexShader处理。 + +假如我们使用这样的顶点数据: + +``` c++ +static float VertexData[] = { + //position(xy) + 1.0f, 1.0f, + 1.0f, -1.0f, + -1.0f, -1.0f, + -1.0f, 1.0f, +}; +``` + +可以使用这样的索引数据,从上述顶点中,使用6个索引来组装新的顶点数据: + +```c++ +static uint32_t IndexData[] = { + 0,1,2, + 2,3,0 +}; +``` + +与VertexBuffer一样,我们只需创建一个IndexBuffer: + +```c++ +QScopedPointer mIndexBuffer; + +mIndexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndexData))); +mIndexBuffer->create(); +``` + +并在首次录制渲染指令时提交索引数据: + +```c++ +batch->uploadStaticBuffer(mIndexBuffer.get(), IndexData); +``` + +渲染指令变成了: + +```c++ +cmdBuffer->setGraphicsPipeline(mPipeline.get()); +cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); +cmdBuffer->setShaderResources(); +const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); + +//指定IndexBuffer,并明确IndexBuffer使用UInt32格式的索引数据 +cmdBuffer->setVertexInput(0, 1, &vertexBindings, mIndexBuffer.get(), 0, QRhiCommandBuffer::IndexUInt32); + +//使用drawIndexed而不是draw,这样渲染会从IndexBuffer中,读取6个索引,利用顶点缓冲区中数据组装出6个顶点,来绘制三角形 +cmdBuffer->drawIndexed(6); +``` + +### Uniform 缓冲区 + +**Uniform 缓冲区(Uniform Buffer)** 用于给流水线提供一些着色器公共的只读数据。 + +在QRhi中,可以使用如下代码来创建一个UniformBuffer: + +``` c++ +QScopedPointer mUniformBuffer; + +mUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, mUniformBuffer)); +mUniformBuffer->create(); +``` + +- UniformBuffer 的类型必须是 `QRhiBuffer::Dynamic` +- `UniformBufferSize` 表示所需缓冲区的字节大小 + +通常在创建UniformBuffer的时候,我们不会直接使用`UniformBufferSize` 来创建一块内存,而是通过一个辅助的结构体定义,比如: + +```c++ +struct UniformBlock{ + QVector2D mousePos; + float time; +}; + +//... +mUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); +//... +``` + +这样做的好处是:可以通过结构体定义来给这段内存一些直观的结构 + +比如上述结构体中,一共申请了`12字节`的内存,其中`前8字节`存储了一个二维向量,`后4字节`存储了一个浮点。 + +Uniform Buffer 属于 **着色器资源(Shader Resource)** ,要使用它,需要在流水线的 着色器资源绑定(即描述符集布局)中,添加一个绑定项: + +```c++ +mShaderBindings.reset(mRhi->newShaderResourceBindings()); +mShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::StageFlag::FragmentStage, mUniformBuffer.get()) +}); +mShaderBindings->create(); +``` + +- QRhiShaderResourceBinding中提供了一些静态方法添加着色器资源绑定项,这里我们使用静态函数`QRhiShaderResourceBinding::uniformBuffer()` +- `0`代表绑定的索引 +- `QRhiShaderResourceBinding::StageFlag::VertexStage`表示该UniformBlock可以在片段着色阶段使用,`Flag`表明这是一个可组合的标识。 + +在C++代码中有了上述结构的支撑,就可以这样在顶点着色器中使用UniformBuffer数据: + +```c++ +QShader vs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(binding = 0) uniform UniformBlock{ // 0 与 ShaderResourceBinding 的定义对应 + vec2 mousePos; + float time; + }UBO; + //... +)"); +Q_ASSERT(vs.isValid()); +``` + +为了能够正确解析UniformBuffer中的内存结构,我们在Shader中定义了一个与C++结构体内存布局一样的 [接口块 (Interface Block)](https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)),上面的是一种简化的写法,下面的写法对大家来说可能会更亲切一些: + +```c++ +QShader vs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 + struct UniformBlock{ + vec2 mousePos; + float time; + }; + layout(binding = 0) uniform UniformBlock UBO; + //... +)"); +Q_ASSERT(vs.isValid()); +``` + +提交UniformBlock数据的方式跟其他Buffer大同小异: + +``` c++ +UniformBlock ubo; +ubo.mousePos = QVector2D(mapFromGlobal(QCursor::pos())) * qApp->devicePixelRatio(); //获取鼠标位置 +ubo.time = QTime::currentTime().msecsSinceStartOfDay() / 1000.0f; //获取当前时间 +batch->updateDynamicBuffer(mUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); +``` + +在上述代码中,我们使用了这样的结构体定义: + +```c++ +struct UniformBlock{ //c++ + QVector2D mousePos; + float time; +}; + +struct UniformBlock{ // shader + vec2 mousePos; + float time; +}; +``` + +需要注意的是,在Shader中定义的块结构,图形API可能会在块成员之间做一些填充来保持硬件对齐,还可能优化掉未使用的成员。 + +在上述结构中,假如我们更换`vec2 mousePos;`和 `float time;`位置, + +```c++ +struct UniformBlock{ //c++ + float time; + QVector2D mousePos; +}; + +struct UniformBlock{ // shader + float time; + vec2 mousePos; +}; +``` + +虽然在结构定义上看上去他们是一样的,但在内存布局上,却有着严重的差异。 + +以OpneGL为例,OpenGL中要求`vec2`遵循`8字节`对齐,这就导致shader中UniformBlock的结构定义,`time`和`mousePos`之间会填充`4字节`以保证`mousePos`是`8字节`对齐,这也意味着shader端的结构体,实际大小并非`12字节`,而是`16字节`,如果此时C++还当成是`12字节`进行处理,就会导致C++端上传的Uniform数据,跟Shader这边收到的数据对不上。 + +要解决这个问题,比较粗暴的方式是在C++侧去手动填充数据来保证对齐: + +```C++ +struct UniformBlock{ //c++ + float time; + uint32_t __padding; //该变量用于填充对齐,保证跟Shader中内存结构的一致 + QVector2D mousePos; +}; + +struct UniformBlock{ // shader + float time; + vec2 mousePos; +}; +``` + +在C++11中,提供了更优雅的方式—— [alignas](https://en.cppreference.com/w/cpp/language/alignas) + +```c++ +struct UniformBlock{ //c++ + float time; + alignas(8) QVector2D mousePos; //使用alignas指定该成员使用8字节对齐 +}; + +struct UniformBlock{ // shader + float time; + vec2 mousePos; +}; +``` + +关于缓冲区对齐,这里有一些更详细的资料: + +- [Learn OpenGL Uniform Block Layout](https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/08%20Advanced%20GLSL/#uniform_1) +- [OpenGL Wiki - Interface_Block ](https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)#Memory_layout) + +### Storage 缓冲区 + +**Storage 缓冲区(Storage Buffer)** 用于在计算管线(Compute Pipelines)中提供可读可写的数据。 + +它的用法与UniformBuffer几乎一模一样,它们之间的主要区别是: + +- **Storage Buffer 可以申请非常大的显存** : OpenGL 规范保证 Uniform Buffer 的大小可以达到 **16KB** ,而 Storage Buffer 可以达到 **128 MB** 。 +- **Storage Buffer 是可写的,甚至支持原子(Atomic)操作** :Storage Buffer 的读写可能是乱序的,因此它们往往需要增加一些内存屏障来保证同步。 +- **Storage Buffer 支持可变存储** :这意味着在Storage Buffer中的块(Block),可以定义一个无界数组,就像是 `int arr[];`, 在着色器中可以使用`arr.length`得到数组长度,而在 Uniform Buffer 中的块,在定义数组时需要明确指定数组大小。 +- **相同条件下,SSBO的访问会比Uniform Buffer要慢** :Storage Buffer 通常像缓冲区纹理一样访问,而 Uniform Buffer 数据是通过内部着色器可访问的内存进行读取。 + +在后面的计算着色器章节,会讲解它的使用,这里有一个完善的文档: + +- https://www.khronos.org/opengl/wiki/Shader_Storage_Buffer_Object + +## 纹理 + +在之前的章节中,我们通过三个顶点特征,借助光栅化插值,来生成了一个彩色的三角形图像: + +![image-20230503194533809](Resources/image-20230503194533809.png) + +而在计算机中,如果我们想绘制一张图片,由于图片中往往具有非常多的特征,而这些特征往往不是线性渐变的,如果继续采用顶点的方式来传递图片的颜色特征,一张图片可能会有上百上千万个三角形,虽然这点数据量对GPU来说不在话下,但这么多数量的三角形,无疑会给几何和光栅化阶段带来巨大的压力 + +例如这是一张大小仅`500*500`的噪声图: + +![image-20230520124250341](Resources/image-20230520124250341.png) + +它的每个颜色都不是线性渐变的,如果以顶点的方式来描述这些特征,那么将需要两百五十万的顶点,这也就意味着图形渲染管线如果想渲染这张图片,将需要处理近百万数量的三角形,很显然,这个操作的性能消耗是非常昂贵的。 + +为了解决图片渲染的难题,图形API提出了另一个概念 —— **纹理(Texture)** + +它以 **着色器资源(Shader Resource)** 的形式存在于图形渲染管线中,与缓冲区相似,它也持有一段GPU上的内存,并且还包含一些特征—— **图像格式(Format),采样数(Sample Count),类型标识(Flags)** + +在QRhi中,可以使用如下接口来创建纹理: + +```c++ +QRhiTexture* QRhi::newTexture(QRhiTexture::Format format, + const QSize &pixelSize, + int sampleCount = 1, + QRhiTexture::Flags flags = {}); +``` + +- **format** :图像格式,常见的图像格式有: + - `RGBA8`:拥有红`R`,绿`G`,蓝`B`和透明度`A`四个通道,`8`表示每个通道占`8 bit`,也就是一字节,它能表示256个特征值 + - `R8`:拥有红色(R)单通道,`8`表示每个通道占`8 bit` + - `RGBA32F`:浮点格式的纹理,拥有红`R`,绿`G`,蓝`B`和透明度`A`四个通道,`32`表示每个通道占`32 bit`,也就是四字节,非浮点纹理只能存储[0,1]之间的值,超出部分会被丢弃,浮点纹理就允许图像上的数值可以越界。 + - `D24S8`:表示用`24 bit`作为深度通道,`8 bit` 作为模板通道 + - ... +- **pixelSize** :想要创建的图像的尺寸。 +- **sampleCount** :采样数,默认为1,不进行多重采样(用于抗锯齿,反走样),一般情况下,电脑会支持设置采样数为{1,2,4,8} +- **flags** :纹理标识,默认情况下,QRhi创建的是一个2维纹理,可以通过这个标识来指定是其他类型,比如一维纹理,三维纹理,立方体纹理,纹理数组等,还能指定纹理的一些行为特征,比如是否生成Mipmap,是否可作为传输源,是否可用于计算着色器读写,是否可用做RenderTarget的附件等。 + +我们可以用这样的代码来创建一张纹理: + +```c++ +QImgae mImage; +QScopedPointer mTexture; + +mImage = QImage("{Your Image Path}").convertedTo(QImage::Format_RGBA8888); +mTexture.reset(mRhi->newTexture(QRhiTexture::RGBA8, mImage.size())); +mTexture->create(); +``` + +为了将图像隐射到几何图形上面,我们会给几何图形的顶点增加一个属性—— **纹理坐标(Texture Coordinate)** + +> 纹理坐标在x和y轴上,范围为0到1之间。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了如何把纹理坐标映射到三角形上的。 +> +> ![img](Resources/tex_coords.png) + +> 为了跟空间位置的坐标分量进行区分,纹理坐标一般不使用x,y,z,而是使用u,v,w,又因为二维纹理比较常用,所以也经常将 纹理坐标 称作 **UV** + +另外,还有一些我们需要考虑的问题: + +- 假如一张存储在磁盘上尺寸为`100*100`的图像,在我们预览的时候放大到了`1000*1000`,那么原本只有一万像素数据的图像,却要在屏幕上显示出一百万像素的效果,这你可能会想到之前的光栅化插值,可现在我们并不是以顶点的方式来绘制图像,而是以着色器纹理资源的方式,插值肯定是要做的,但以什么插值方式进行呢? +- 假如我们要对一个图像上的每个点都执行这样的操作:每个像素点都算一遍跟邻近8个像素的平均值。这看似简单的操作却有一个非常麻烦的问题,边界的一圈像素周边并没有8个像素点,所以就意味着要对他们做特殊处理,而特殊处理就意味着存在逻辑分支,还记得上一章节所说的吗? GPU 对逻辑处理并不友好。那有没有其他办法呢,如果可以在采样边界部分空缺像素的时候,也能正常采样,返回一个值(比如说是0),那就可以不用对边界做特殊处理了。这个操作就意味着我们可能会超出纹理坐标的范围[0,1]去对图像进行采样,那这种情况的采样,该返回什么值合适呢? + +这就是 **采样器(Sampler)** 的职责所在 —— 定义 **插值过滤行为** 和 **越界处理策略** + +这里有一个非常好的文章介绍了这些行为,请读者务必查看: + +- https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/#_3 + +在QRhi中,可以使用下方函数来创建采样器: + +```c++ +enum Filter { + None, + Nearest, //邻近过滤(Nearest Neighbor Filtering),取距离纹理坐标最近的像素值 + Linear //线性过滤(Linear Filtering),根据纹理坐标周边的像素进行插值 +}; +enum AddressMode { + Repeat, //重复,越界部分会使用重复的图像 + ClampToEdge, //约束到边界:越界部分会使用图形边界的像素值 + Mirror, //镜像:越界部分会使用图像的镜像 +}; +QRhiSampler*QRhi::newSampler(QRhiSampler::Filter magFilter, + QRhiSampler::Filter minFilter, + QRhiSampler::Filter mipmapMode, + QRhiSampler::AddressMode addressU, + QRhiSampler::AddressMode addressV, + QRhiSampler::AddressMode addressW = QRhiSampler::Repeat); +``` + +- **magFilter** :放大(Magnify)过滤,即图像放大时采样何种方式进行采样过滤 +- **minFilter** :缩小(Minify)过滤,即图像缩小时采样何种方式进行采样过滤 +- **mipmapMode** :多级渐远纹理的采样模式,下一篇文章会细说。 +- **addressU** :在水平方向的越界处理策略 +- **addressV** :在竖直方向的越界处理处理 +- **addressW** :3D空间中,垂直于屏幕方向的越界处理策略,二维纹理可无视这个参数。 + +可以使用这样的方式来创建采样器: + +```c++ +QScopedPointer mSapmler; + +mSapmler.reset(mRhi->newSampler( + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::Nearest, + QRhiSampler::AddressMode::Repeat, + QRhiSampler::AddressMode::Repeat, + QRhiSampler::AddressMode::Repeat +)); +mSapmler->create(); +``` + +而纹理跟 UniformBuffer 一样,都属于 **着色器资源(Shader Resource)** ,使用它也需要在 **着色器资源绑定(ShaderResourceBinding)** 中进行绑定: + +```c++ +mShaderBindings->setBindings({ + QRhiShaderResourceBinding::sampledTexture(1, //流水线中绑定的索引 + QRhiShaderResourceBinding::StageFlag::FragmentStage, //可在片段着色器中使用 + mTexture.get(), //纹理对象 + mSapmler.get()) //采样器对象 +}); +``` + +有了上述代码的支撑,我们可以在片段着色器中使用如下代码去采样纹理: + +```glsl +layout(location = 0) in vec2 vUV; //从顶点着色阶段经光栅化传递过来的纹理坐标 +layout(location = 0) out vec4 outFragColor; //片段颜色输出 + +layout(binding = 1) uniform sampler2D inTexture; //着色器纹理资源 + +void main(){ + outFragColor = texture(inTexture,vUV); //通过texture函数根据UV坐标对图像采样,得到值的类型是vec4 +} +``` + +这里有几个有用小Tips: + +- GLSL中可以使用函数`textureSize(inTexture, 0)`获取纹理的尺寸 +- 在一些章节中测试一些Pass的效果可能会经常要到全屏纹理的绘制,有一个简单的方法,在不使用任何顶点输入的情况下,根据顶点着色器的内置变量`gl_VertexIndex`去生成一个矩形的顶点和纹理坐标,只需要用一个空的 **InputVertexLayout** ,并调用`cmdBuffer->draw(4)`就能绘制矩形纹理,其中顶点着色器的创建如下: + +```c++ +QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 450 + layout (location = 0) out vec2 vUV; + out gl_PerVertex{ + vec4 gl_Position; + }; + void main() { + vUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); / + gl_Position = vec4(vUV * 2.0f - 1.0f, 0.0f, 1.0f); +#if Y_UP_IN_NDC //因为DX和GL,VK的NDC坐标不一致,因此这里需要做一些兼容处理 + vUV.y = 1 - vUV.y; +#endif + })" + , QShaderDefinitions() //该参数只是简单的在代码开头添加 #define Y_UP_IN_NDC 1 + .addDefinition("Y_UP_IN_NDC", mRhi->isYUpInNDC()) +); +``` + +> 详见:https://stackoverflow.com/questions/2588875/whats-the-best-way-to-draw-a-fullscreen-quad-in-opengl-3-2 + +## 示例 + +在该教程的[04-BufferAndTexture](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/04-BufferAndTexture/Source/main.cpp)示例中,演示了如何使用 **Uniform Buffer** , **Index Buffer** 和 **Texture** ,你可以以它为参考,去实现一些有意思的东西: + +![418](Resources/418.gif) + +## 内存管理 + +在 [C++内存管理](https://zhuanlan.zhihu.com/p/603325037) 的章节中,我们介绍了一些C++中管理CPU内存的方法,而在GPU中,它的内存使用同样也有一些黑话,这里有一些详细的文档: + +- **Vulkan中该做和不该做的事情** :https://developer.nvidia.com/blog/vulkan-dos-donts/ +- **DX12中该做和不该做的事情** :https://developer.nvidia.com/dx12-dos-and-donts + +- **Vulkan 内存管理** :https://www.youtube.com/watch?v=rXSdDE7NWmA + - B站转载:https://www.bilibili.com/video/BV17W411S7a1/?p=3 + - 演讲PPT:https://www.khronos.org/assets/uploads/developers/library/2018-vulkan-devday/03-Memory.pdf + +同样也有一些我们可以依赖的三方库: + +- **Vulkan 内存分配器** :https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator +- **D3D12 内存分配器** :https://github.com/GPUOpen-LibrariesAndSDKs/D3D12MemoryAllocator + +> 幸运的是,QRhi已经在内部装载了这两个内存分配器:https://github.com/qt/qtbase/tree/dev/src/gui/rhi + diff --git "a/Docs/01-GraphicsAPI/5.\344\270\211\347\273\264\347\251\272\351\227\264.md" "b/Docs/01-GraphicsAPI/5.\344\270\211\347\273\264\347\251\272\351\227\264.md" new file mode 100644 index 00000000..bd6b0362 --- /dev/null +++ "b/Docs/01-GraphicsAPI/5.\344\270\211\347\273\264\347\251\272\351\227\264.md" @@ -0,0 +1,549 @@ +--- +comments: true +--- + +# 三维空间 + +## 3D变换 + +现代计算机的图形显示器都是在显示二维的图像,事实上,投影在人体视网膜表面的图像也是二维的,那为什么人体能从二维的图像上感受到距离感和空间感呢? + +这是因为我们的大脑往往能捕获图像上的一些细节,从而重建三维空间的信息,就比如: + +- 近大远小的空间透视感 + + ![image-20230527223242963](Resources/image-20230527223242963.png) + +- 光影产生的立体感 + + ![image-20230528152235000](Resources/image-20230528152235000.png) + +除了单帧图像所携带的信息,人眼还会根据图像的运动情况来还原空间的信息,就比如运动视差: + +![undefined](Resources/Parallax_scroll.gif) + +当然,还有一些其他因素,这里暂不展开讨论,感兴趣的小伙伴可以查阅: + +- Wiki Depth Perception:https://en.wikipedia.org/wiki/Depth_perception + +计算机的成像过程如下: + +![img](Resources/viewing-frustum-_frustum.fit_lim.size_768x.gif) + +我们可以简单看作是,把3D空间的物体,拍扁贴到`近平面(Near Plane)`上 + +在理论层面,3D空间中的物体在计算机中不过是一堆数据,贴到 近平面 的图像又是另一堆数据,而我们要做的,无非是将一堆数据转换成另一堆数据 + +回顾一下我们初高中在学校所学习过的知识,思考以下场景: + +- 将 一个三角形 水平平移 5 个单位,你会怎么做? + - 很简单,三角形的所有顶点坐标的X值 `加5` 就行 + +- 将 一个三角形 绕坐标原点旋转180度,怎么处理? + - 可以把三角形坐标从直角(笛卡尔)坐标系转换为极坐标系,角度 `增加180度` ,再转换回直接坐标系,就能得到转换后的顶点 +- 将 一个三角形 放大到原先的两倍,该这么办? + - 所有顶点的坐标值都 `乘2` 就行了 + +上述的几种情况包含了图形数据的大部分处理方式,你应该也能发现它们的处理流程都是固定模板的,在计算机中,固定模板的执行流程就意味着可以在硬件级别做大量优化。 + +所有我们可以再想想,能不能上面的流程能不能再简化一下?使用一个模板就能执行所有可能的操作,这样计算机就能针对这一模板,进行极致的优化。 + +答案肯定是有的,而这套模板就建立在 [线性代数](https://baike.baidu.com/item/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0) 的体系之上 + +对于一个三维坐标`(X,Y,Z)`,如果我们想让它平移`(Tx,Ty,Tz)`个单位,会给该坐标补上一个[齐次坐标(Homogeneous_Coordinates)](https://en.wikipedia.org/wiki/Homogeneous_coordinates),然后乘上一个 **平移矩阵(Translation Matrix)** : + +![image-20210521111108645](Resources/5e8862ed2174022bee08dd884f4300cf.png) + +如果想让它在不同坐标分量上,缩放`(S1,S2,S3)`个大小,会给它乘上一个 **缩放矩阵(Scale Matrix)** : + +![image-20210521105821765](Resources/1df7ad4e15915530a451f053bbcfeea8.png) + +如果想绕它绕X轴旋转θ角度,会给它乘上一个 **旋转矩阵(Rotate Matrix)** : + +![image-20210521111545555](Resources/21ae934529205b73ae6d048c89898edb.png) + +如果是Y轴: + +![image-20210521111559180](Resources/aa579998d7db8ba46a9a96fe4c292232.png) + +Z轴: + +![image-20210521111610922](Resources/9ef98d9520af66ac686cd86f70e85803.png) + +在C++中,大多数数学库都提供了向量和矩阵的各类便捷函数,在Qt中,就提供了以下数据结构: + +- [QVector2D](https://doc.qt.io/qt-6/qvector2d.html) +- [QVector3D](https://doc.qt.io/qt-6/qvector3d.html) +- [QVector4D](https://doc.qt.io/qt-6/qvector4d.html) +- [QMatrix4x4](https://doc.qt.io/qt-6/qmatrix4x4.html) + +比如这样的代码: + +``` c++ +QVector4D pos(1, 1, 1, 1); + +QMatrix4x4 model; +model.translate(QVector3D(1,0,0)); +model.scale(0.5); +model.rotate(180, QVector3D(1, 0, 0)); + +QVector4D result = model * pos; +``` + +代表的意义是: + +- 使用`model矩阵`先让`pos向量`绕X轴旋转180度,再将其缩放到原先的0.5倍,最后向X轴平移1个单位 + +由于Qt中的向量使用的是 **按列存储** ,所以我们需要通过 **左乘** 的方式来处理变换,所以整个过程看起来像是反了一样 + +还记得矩阵的乘法口诀吗?—— **前行乘后列** + +根据这一特性,我们实际想要的是:一个`向量`乘以一个`矩阵`,得到的是一个变换后的`向量` + +下图从 乘法计算 结果 的 类型 说明了为什么对于 **按列存储的向量需要使用左乘** : + +![image-20230528104226972](Resources/image-20230528104226972.png) + +虽然说对3D空间的变换是通过矩阵进行的,但如果把所有变换都糅合到一个矩阵里面,就回导致变换的二次处理加工变得非常困难。 + +因此,现代图形学中,3D空间的变换还划分为几个阶段,这几个阶段有着不同的处理职责: + +![coordinate_systems](Resources/coordinate_systems.png) + +- 图形原先所处的空间,我们称作 **局部空间(Local Space)** ,我们可以通过 **模型矩阵(Model Matrix)** 将它的坐标转换到 **世界空间(World Space)** , **模型矩阵的职责是处理物体的平移,旋转,缩放** 。 +- 通过 **视图矩阵(View Matrix)** , 我们可以将 **世界坐标(World Coordinate)** 转换到 **视图空间(View Space)** 中的坐标, **视图矩阵的职责是确立视点所在的位置和视野的朝向** +- 再通过 **投影矩阵(Projection Matrix)** ,可以将 **观察坐标(View Coordinate)** 转换到 **裁剪空间(Clip Space)** 中的坐标, **投影矩阵的职责是把视图空间的区域投影到标准设备坐标系(NDC)中** +- **裁剪坐标(Clip Coordinate)** 经由 **视口变换(Viewport Transform)** 最终变换到 **屏幕空间(Screen Space)** 中 + +在Qt中,我们可能会使用如下代码在CPU侧处理这些变换: + +``` c++ +QMatrix4x4 corrMatrix = mRhi->clipSpaceCorrMatrix(); //裁剪空间矫正矩阵 + +QMatrix4x4 project ; +// 设置Fov为90度,传入FrameBuffer的宽高比,设置近平面距离为0.01,远平面距离为1000 +project.perspective(90.0, renderTarget->pixelSize().width() /(float) renderTarget->pixelSize().height(), 0.01, 1000); + +QMatrix4x4 view; +// 从位置(10,0,0) 看向 (0,0,0),视线的上向量为竖直向上(Y轴正方向) +view.lookAt(QVector3D(10, 0, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 0)); + +QMatrix4x4 model; +model.translate(QVector3D(1, 0, 0)); +model.scale(0.5); +model.rotate(180, QVector3D(1, 0, 0)); + +UniformBlock ubo; +ubo.MVP = (corrMatrix * project * view * model).toGenericMatrix<4, 4>(); +batch->updateDynamicBuffer(mUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); +``` + +在Shader中这样使用: + +```c++ +QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec3 inPosition; + layout(binding = 0) uniform UniformBlock{ + mat4 MVP; + }UBO; + out gl_PerVertex { vec4 gl_Position; }; + void main() + { + gl_Position = UBO.MVP * vec4(inPosition ,1.0f); + } +)"); +``` + +### 标准化设备坐标(Normalized Device Coordinates) + +在前面的章节中,我们使用了这样的顶点数据: + +``` c++ +static float VertexData[] = { //顶点数据 + //position(xy) color(rgba) + 0.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, + -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, +}; +``` + +和顶点着色器: + +```cpp +QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; //这里需要与上面的inputLayout 对应 + layout(location = 1) in vec4 color; + + layout (location = 0) out vec4 vColor; //输出变量,这里的location是out的,而不是in + + out gl_PerVertex { //Vulkan GLSL中固定的定义 + vec4 gl_Position; + }; + + void main(){ + gl_Position = vec4(position,0.0f,1.0f); //根据输入的position,设置实际的顶点输出 + vColor = color; //将输入的color传递给fragment shader + } +)"); +``` + +绘制出了一个三角形: + +![image-20230503194533809](Resources/image-20230503194533809.png) + +大家可能已经猜到了顶点位置里面 `0.0f`,-`0.5f`,`0.5f`所代表的意义,仿佛这些顶点坐标的取值范围是[-1,1]?这是因为图形API为了让坐标运算不受显示器分辨率的影响,都使用了 **标准化设备坐标(Normalized Device Coordinates, 简称NDC)** 的概念。 + +以OpenGL为例,它遵循右手坐标系: + +![image-20230521094519474](Resources/image-20230521094519474.png) + +任一轴向的取值范围为[-1,1],就像这样: + +![image-20230521093725199](Resources/image-20230521093725199.png) + +任何落在范围外的 **顶点** 会按一些策略进行图元裁剪,而不会显示在屏幕上 : + +被剪裁的三角形 + +该过程详见: + +- https://registry.khronos.org/vulkan/specs/1.3/html/chap24.html + +Vulkan也使用右手坐标系,因为Vulkan声称是下一代的OpenGL,跟OpenGL一样怎么好意思叫Vulkan,它总要跟OpenGL有一些标新立异的点,因此, **Vulkan的Y轴是朝下的** ,为了跟其他现代图形API保持一致, **它的Z轴(深度) 取值范围变成了[0,1]** ,就是这个样子: + +![image-20230527105811415](Resources/image-20230527105811415.png) + +而 DirectX 和 Metal 又与它们不同, **DirectX 和 Metal的NDC使用的是左手坐标系,且深度值范围是[0,1]** : + +![image-20230521100350075](Resources/image-20230521100350075.png) + +关于它们的差异和细节,详见: + +- https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/coordinate-systems.html + +#### NDC差异解决方案 + +这些 图形API 标准坐标系的差异,无疑给RHI的封装增加了一些难题,各个引擎对此都有自己的解决方案。 + +在QRhi中,我们在使用时只需要顶点数据统一遵循OpenGL的NDC(即右手坐标系,任一轴向的取值范围为[0,1]) + +通过`QRhi::clipSpaceCorrMatrix()` 可以获取到一个当前图形API的矫正矩阵,使用它对所有顶点都处理一遍可以将之转换到对应API的NDC空间。 + +该功能依赖于QRhi中的以下实现: + +``` c++ +QMatrix4x4 QRhiGles2::clipSpaceCorrMatrix() const +{ + return QMatrix4x4(); // identity +} + +QMatrix4x4 QRhiVulkan::clipSpaceCorrMatrix() const +{ + // See https://matthewwellings.com/blog/the-new-vulkan-coordinate-system/ + + static QMatrix4x4 m; + if (m.isIdentity()) { + // NB the ctor takes row-major + m = QMatrix4x4(1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, -1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.5f, 0.5f, + 0.0f, 0.0f, 0.0f, 1.0f); + } + return m; +} + +QMatrix4x4 QRhiD3D11::clipSpaceCorrMatrix() const +{ + // Like with Vulkan, but Y is already good. + + static QMatrix4x4 m; + if (m.isIdentity()) { + // NB the ctor takes row-major + m = QMatrix4x4(1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.5f, 0.5f, + 0.0f, 0.0f, 0.0f, 1.0f); + } + return m; +} + +QMatrix4x4 QRhiD3D12::clipSpaceCorrMatrix() const +{ + // Like with Vulkan, but Y is already good. + + static QMatrix4x4 m; + if (m.isIdentity()) { + // NB the ctor takes row-major + m = QMatrix4x4(1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.5f, 0.5f, + 0.0f, 0.0f, 0.0f, 1.0f); + } + return m; +} + +QMatrix4x4 QRhiMetal::clipSpaceCorrMatrix() const +{ + // depth range 0..1 + static QMatrix4x4 m; + if (m.isIdentity()) { + // NB the ctor takes row-major + m = QMatrix4x4(1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.5f, 0.5f, + 0.0f, 0.0f, 0.0f, 1.0f); + } + return m; +} +``` + +对于一些二维的顶点,我们可以简单的翻转一下坐标的Y值,这也是上一节中纹理 [无属性渲染](https://link.zhihu.com/?target=https%3A//stackoverflow.com/questions/2588875/whats-the-best-way-to-draw-a-fullscreen-quad-in-opengl-3-2) ,为什么要用`Y_UP_IN_NDC`对顶点翻转的来由: + +```c++ +QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 450 + layout (location = 0) out vec2 vUV; + out gl_PerVertex{ + vec4 gl_Position; + }; + void main() { + vUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); / + gl_Position = vec4(vUV * 2.0f - 1.0f, 0.0f, 1.0f); +#if Y_UP_IN_NDC //因为DX,GL与VK的NDC不一致,因此这里需要做一些兼容处理 + gl_Position.y = - gl_Position.y; +#endif + })" + , QShaderDefinitions() //该参数只是简单的在代码开头添加 #define Y_UP_IN_NDC 1 + .addDefinition("Y_UP_IN_NDC", mRhi->isYUpInNDC()) +); +``` + +### Frame Buffer 坐标 差异 + +此外,我们还需要注意图形API之间不仅仅有NDC的差异,FrameBuffer的坐标空间也有不同,由于[历史原因](https://gamedev.stackexchange.com/questions/83570/why-is-the-origin-in-computer-graphics-coordinates-at-the-top-left),早期的计算机阴极射线管(CRT)从左上角到右上角绘制图像,所以在大多数传统图形API,通常 **以左上角为帧图像的坐标原点** ,OpenGL成立之初想要改变这一点,让坐标系的使用更符合人类的通识(即二维平面上,使用左下角作为坐标原点,Y轴朝上,X轴朝右),但可惜的是,已经有太多的框架已经适应并普遍遵循原先的规定,这反倒导致了我们在 **使用 OpenGL 的时候还需要对纹理进行上下翻转(Flip Y)** ,在现代图形API (DirectX、Metal、Vulkan), 都已经向历史妥协,所以大家在使用时注意遵循 ”传统“: + +![image-20230521104752631](Resources/image-20230521104752631.png) + +### 示例 + +根据上面的一些描述,我们可以制作一些简单的小程序,比如一个随时间变换的立方体: + +![118](Resources/118.gif) + +示例代码位于 [05-3D](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/05-3D/Source/main.cpp) + +核心代码只是: + +``` c++ +QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec3 inPosition; + layout(binding = 0) uniform UniformBlock{ + mat4 MVP; + }UBO; + out gl_PerVertex { vec4 gl_Position; }; + void main() + { + gl_Position = UBO.MVP * vec4(inPosition ,1.0f); + } +)"); +Q_ASSERT(vs.isValid()); +``` + +``` c++ +QMatrix4x4 corrMatrix = mRhi->clipSpaceCorrMatrix(); //裁剪空间矫正矩阵 + +QMatrix4x4 project; +project.perspective(90.0, renderTarget->pixelSize().width() / (float)renderTarget->pixelSize().height(), 0.01, 1000); // 设置Fov为90度,传入FrameBuffer的宽高比,设置近平面距离为0.01,远平面距离为1000 + +QMatrix4x4 view; +view.lookAt(QVector3D(10, 0, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 0)); // 从位置(10,0,0) 看向 (0,0,0),视线的上向量为竖直向上(Y轴正方向) + +float time = QTime::currentTime().msecsSinceStartOfDay() / 1000.0f; //获取当前时间的秒数(浮点类型) +float factor = qAbs(qSin(time)); //利用正弦函数让Y值随时间在[0,1]之间变化 +QMatrix4x4 model; +model.translate(QVector3D(0, factor * 5, 0)); //随时间上下移动 +model.scale(factor); //随时间缩放 +model.rotate(time * 180, QVector3D(1, 1, 1)); //随时间旋转 + +UniformBlock ubo; +ubo.MVP = (project * view * model).toGenericMatrix<4, 4>(); +batch->updateDynamicBuffer(mUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); +``` + + + +### 建议与技巧 + +上述文章并不完备,这里还有一些相关参考: + +- https://learnopengl-cn.github.io/01%20Getting%20started/08%20Coordinate%20Systems/ + +3D空间变换涉及到大量的线性代数运算,里面的一些概念和细节,很难用一篇文章去涵盖,这里笔者再次推荐读者一定要去阅读一下《3D数学基础 图形和游戏开发 (第2版)》的前十章内容,它里面涵盖了图形学所必须的极大部分基础知识,认真过一遍,保证你的知识体系里面没有盲区~ + +对于一个没有经历过透视变换的矩阵,它的平移,旋转,缩放参数是可提取的,在ImGuiZmo中有以下代码: + +```c++ +void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale) +{ + matrix_t mat = *(matrix_t*)matrix; + + scale[0] = mat.v.right.Length(); + scale[1] = mat.v.up.Length(); + scale[2] = mat.v.dir.Length(); + + mat.OrthoNormalize(); + + rotation[0] = RAD2DEG * atan2f(mat.m[1][2], mat.m[2][2]); + rotation[1] = RAD2DEG * atan2f(-mat.m[0][2], sqrtf(mat.m[1][2] * mat.m[1][2] + mat.m[2][2] * mat.m[2][2])); + rotation[2] = RAD2DEG * atan2f(mat.m[0][1], mat.m[0][0]); + + translation[0] = mat.v.position.x; + translation[1] = mat.v.position.y; + translation[2] = mat.v.position.z; +} +``` + +当我们想要实现一个始终面向相机公告牌效果时,可以将View矩阵中的Rotate信息给去掉,如果不希望随相机的距离缩放,可以把Scale信息也给去掉,或者说直接这样(伪代码): +```glsl +mat4 Model,View,Projection; +vec3 position; + +mat3 CorrMatrix = inverse(View); //丢弃平移参数,求它的逆矩阵来矫正旋转和缩放 +vec3 facingCameraPos = Projection * View * Model * (CorrMatrix * position, 1.0f); +``` + +## Camera + +相信接触过3D场景的小伙伴大多知道 **摄像机(Camera)** 的概念,在很多3D游戏中都提供这样的操作: + +- 使用 WASD 移动相机的位置,使用鼠标来控制视角的旋转 + +而 Camera 的本质其实是在提供上文所提到的 **视图矩阵(View Matrix)** 和 **投影矩阵(Projection Matrix)** + +我们可以窗口系统的输入事件,来调整这些矩阵。 + +这里有笔者封装好的一个Camera类: + +- https://github.com/Italink/QEngineUtilities/blob/main/Core/Src/Utils/QCamera.cpp + +``` c++ +class QCamera :public QObject { + Q_OBJECT + Q_PROPERTY(QVector3D Position READ getPosition WRITE setPosition) + Q_PROPERTY(QVector3D Rotation READ getRotation WRITE setRotation) + Q_PROPERTY(float FOV READ getFov WRITE setFov) + Q_PROPERTY(float NearPlane READ getNearPlane WRITE setNearPlane) + Q_PROPERTY(float FarPlane READ getFarPlane WRITE setFarPlane) + Q_PROPERTY(float MoveSpeed READ getMoveSpeed WRITE setMoveSpeed) + Q_PROPERTY(float RotationSpeed READ getRotationSpeed WRITE setRotationSpeed) +public: + QCamera(); + + void setupWindow(QWindow* window); + + float getYaw(); + float getPitch(); + float getRoll(); + + void setYaw(float inVar); + void setPitch(float inVar); + void setRoll(float inVar); + + QVector3D getPosition(); + void setPosition(const QVector3D& newPosition); + + void setRotation(const QVector3D& newRotation); + QVector3D getRotation(); + + float getRotationSpeed() const { return mRotationSpeed; } + void setRotationSpeed(float val) { mRotationSpeed = val; } + + float getMoveSpeed() const{ return mMoveSpeed; } + void setMoveSpeed(float val) { mMoveSpeed = val; } + float& getMoveSpeedRef() { return mMoveSpeed; } + + void setFov(float val); + float getFov() { return mFov; } + + void setAspectRatio(float val); + float getAspectRatio() { return mAspectRatio; } + + void setNearPlane(float val); + float getNearPlane() { return mNearPlane; } + + void setFarPlane(float val); + float getFarPlane() { return mFarPlane; } + + QMatrix4x4 getProjectionMatrixWithCorr(QRhiEx* inRhi); + QMatrix4x4 getProjectionMatrix(); + QMatrix4x4 getViewMatrix(); +}; +``` + +要想使用它,只需使用创建一个QCamera,并调用`void QCamera::setupWindow(QWindow* window)` 就能将窗口的输入事件跟Camera进行绑定,之后我们可以通过以下函数来获取模型矩阵和投影矩阵: + +```c++ +QMatrix4x4 getProjectionMatrixWithCorr(QRhiEx* inRhi); //获取带裁剪空间矫正的透视矩阵 +QMatrix4x4 getProjectionMatrix(); +QMatrix4x4 getViewMatrix(); +``` + +如果你想封装自己的Camera类,这里有一个很好的文章: + +- https://learnopengl-cn.github.io/01%20Getting%20started/09%20Camera/ + +### Mipmap + +在上一节中 [缓冲区与纹理](https://zhuanlan.zhihu.com/p/630886324) 中,通过 **minFilter** 可以确定在图像被缩小显示时,使用何种策略来根据纹理坐标的 **邻近像素** 确定当前的像素值,使用线性(Linear)采样会根据像素周边四个的邻近像素来确定当前颜色,可是当图像的缩放比例非常大的时候,比如将一张图像的寸尺从`1000*1000`,缩放到`10*10`,使用Linear采样的时候,只会根据四个邻近像素点来确定颜色,明显会导致采样不足,出现 [摩尔纹](https://baike.baidu.com/item/%E6%91%A9%E5%B0%94%E7%BA%B9) 现象,就像这样: + +![14568](Resources/14568.gif) + +为了解决这个问题,现代图形API引入了另一个概念 — **多级渐远纹理(Mipmap)** + +开启Mipmap会比原纹理多约三分之一的空间大小,,它的原理可以看作是将图像每次缩放到原先图像一半的新图像作为一个 **Mip Level** ,就像是这样: + +![img](Resources/mipmaps.png) + +图形渲染管线在执行时,会根据uv和texture尺寸,选用合适的 Mip Level 进行采样 + +在QRhi中,我们只需要在创建纹理使用`QRhiTexture::Flag::MipMapped | QRhiTexture::UsedWithGenerateMips`: + +```c++ +mTexture.reset(mRhi->newTexture(QRhiTexture::RGBA8, + mImage.size(), + 1 , + QRhiTexture::Flag::MipMapped | QRhiTexture::UsedWithGenerateMips )); +mTexture->create(); +``` + +并在采样器创建时,指定 Mip Level 之间的过滤模式: + +``` c++ +mSampler.reset(mRhi->newSampler( + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::Nearest, + QRhiSampler::Filter::Linear, //使用线性过滤 + QRhiSampler::AddressMode::Repeat, + QRhiSampler::AddressMode::Repeat, + QRhiSampler::AddressMode::Repeat +)); +mSampler->create(); +``` + +最后在录制 图像资源的上传指令 后,使用`QRhiResourceUpdateBatch::generateMips(QRhiTexture *tex)`录制mips的生成指令: + +``` c++ +batch->uploadTexture(mTexture.get(), mImage); +batch->generateMips(mTexture.get()); +``` + +再执行程序一看,可以看到摩尔纹已经消失了: + +![1468](Resources/1468.gif) + +示例代码位于该教程的 : + +- [06-3DWithCamera](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/06-3DWithCamera/Source/main.cpp) + +关于 Mipmap,这里有一篇很好的文章: + +- [Clawko - 图形学底层探秘 - 纹理采样、环绕、过滤与Mipmap的那些事](https://zhuanlan.zhihu.com/p/143377682) diff --git "a/Docs/01-GraphicsAPI/6.\345\233\276\345\275\242\346\270\262\346\237\223\350\277\233\351\230\266.md" "b/Docs/01-GraphicsAPI/6.\345\233\276\345\275\242\346\270\262\346\237\223\350\277\233\351\230\266.md" new file mode 100644 index 00000000..1d73529e --- /dev/null +++ "b/Docs/01-GraphicsAPI/6.\345\233\276\345\275\242\346\270\262\346\237\223\350\277\233\351\230\266.md" @@ -0,0 +1,926 @@ +--- +comments: true +--- + +在之前的章节中,大家应该都已经熟悉了如何在C++中搭建现代图形API的基本渲染流程,通过使用 [QRhiGraphicsPipeline](https://alpqr.github.io/qtrhi/qrhigraphicspipeline.html) 和 [QRhiCommandBuffer](https://alpqr.github.io/qtrhi/qrhicommandbuffer.html) ,并编写 Vertex Shader 和 Fragment Shader,对图形渲染管线也有了一定认知: + +![graphics-pipeline](Resources/graphics-pipeline.png) + +图形渲染管线本质上只是进行数据的处理,它并没有图形和空间的概念,开发者只是利用这个高效的数据处理器,根据自己的思维去定制相应的流程,从而得到预期的产物。 + +## 图元拓扑(Primitive Topology) + +点,线,面是三维图形的基本单位,在图形API中,我们可以通过设置图元拓扑,来决定流水线产出的图元机制。 + +``` c++ +mPipeline->setTopology(QRhiGraphicsPipeline::Topology::Triangles); +``` + +图元拓扑就决定了在几何装配阶段,流水线如何挑选顶点来组装基础图元,在QRhi中,支持以下几种拓扑策略: + +```c++ +enum Topology { + Triangles, + TriangleStrip, + TriangleFan, + Lines, + LineStrip, + Points, + Patches //用于镶嵌控制和评估着色器 +}; +``` + +QRhi默认使用的是 `Triangles`,假设现在有六个顶点(下方使用索引描述),不同拓扑对应的组装策略是: + +- **Triangles** :组装得到2个三角形`{0,1,2},{3,4,5}` +- **TriangleStrip** :组装得到4个三角形`{0,1,2},{2,1,3}{2,3,4},{4,3,5}`,顶点的异常索引顺序,是由于流水线会依据顶点的时钟顺序来判断三角形的正反面,所以在组装TriangleStrip的时候会确保时钟顺序不变。 +- **TriangleFan** :组装得到4个三角形`{0,1,2},{0,2,3},{0,3,4},{0,4,5}` +- **Lines** :组装得到3条线`{0,1},{2,3},{4,5}` +- **LineStrip** :组装得到5条线`{0,1},{1,2},{2,3},{3,4},{4,5}` +- **Points** :组装得到6个点`{0},{1},{2},{3},{4},{5}` + +这里有一个Vulkan中的图示: + +![img](Resources/905430d6d8ca6d241f51eab02af3be71232a0517.png@942w_683h_progressive-1694935064528-4.webp) + +## 几何模式(Polygon Mode) + +默认图形渲染管线使用的是填充模式(Fill Mode),绘制的三角形图元会进行填充。 + +image-20230911202544320 + +图形API往往还支持 线框模式(Line Mode),该模式只会绘制三角形图元的边线。 + +image-20230911202700715 + +在QRhi中的使用方式如下: + +``` C++ +mPipeline->setPolygonMode(QRhiGraphicsPipeline::Line); +``` + +## 片段操作(Fragment Operations) + +假如我们画了两个矩形: + +![image-20230830194119839](Resources/image-20230830194119839-1694935064528-7.png) + +我们可能会有以下需求: + +- 如果出现重叠,让后画的矩形将之前的矩形覆盖 + + ![image-20230830194150854](Resources/image-20230830194150854-1694935064528-8.png) + +- 如果出现重叠,让矩形的颜色按照一定策略混合 + + ![image-20230830194235278](Resources/image-20230830194235278-1694935064528-9.png) + +- 让矩形按照一定的空间遮挡关系进行显示 + + ![image-20230830194744480](Resources/image-20230830194744480-1694935064528-11.png) + +- 让前画的矩形作为蒙版,来确定后方矩形的绘制区域 + + ![image-20230830194643278](Resources/image-20230830194643278-1694935064528-10.png) + +图形API也提供了这些功能的支持,在图形渲染管线中,光栅化产生的片段将经过一些[片段操作(Fragment Operations)](https://registry.khronos.org/vulkan/specs/1.3/html/chap26.html#fragops),来确定是否或如何在帧缓冲上写入片段着色器产生的值,这些操作通常按如下顺序进行: + +1. [裁剪测试(Scissor test)](https://registry.khronos.org/vulkan/specs/1.3/html/chap26.html#fragops-scissor):通过一个矩形区域来裁剪图像。 +2. [深度边界测试(Depth bounds test)](https://registry.khronos.org/vulkan/specs/1.3/html/chap26.html#fragops-dbt):根据深度值范围来裁剪图像。 +3. [模板测试(Stencil test)](https://registry.khronos.org/vulkan/specs/1.3/html/chap26.html#fragops-stencil):可以通过模板(遮罩:Mask),实现异形区域的裁剪。 +4. [深度测试(Depth test)](https://registry.khronos.org/vulkan/specs/1.3/html/chap26.html#fragops-depth):通过比较深度值来筛选片段,从而让图像表现出前后遮挡的空间关系。 +5. [混合(Blending)](https://registry.khronos.org/vulkan/specs/1.3/html/chap27.html#framebuffer-blending):通过一些策略来混合重叠的片段。 + +> 需要注意的是:上面只是比较常见且对开发者作用较大的片段操作,这里有一个完整的描述文档: +> +> - https://registry.khronos.org/vulkan/specs/1.3/html/chap26.html#fragops + +这里我们主要是要学会怎么去配置 **重叠片段的处理机制** 和 **片段的筛选手段** 。 + +### 裁剪测试(Scissor Test) + +- 使用裁剪测试的目的很简单:就是希望通过一个矩形对图像进行裁剪,就像是这样: + +![image-20230907194807029](Resources/image-20230907194807029-1694935064528-12.png) + +在QRhi中,使用裁剪测试只需要在创建流水线时设置`QRhiGraphicsPipeline::Flag::UsesScissor`: + +``` c++ +mPipeline->setFlags(QRhiGraphicsPipeline::Flag::UsesScissor); +``` + +绘制时使用`QRhiCommandBuffer::setScissor(const QRhiScissor &scissor)`: + +```C++ +cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, batch); +cmdBuffer->setGraphicsPipeline(mPipeline.get()); +cmdBuffer->setViewport(QRhiViewport(0, 0, width, height)); +cmdBuffer->setScissor(QRhiScissor(0, 0, width/2, height/2)); //通过裁剪只保留四分之一区域 +cmdBuffer->setShaderResources(mCubeShaderBindings.get()); +const QRhiCommandBuffer::VertexInput vertexInput(mCubeVertexBuffer.get(), 0); +cmdBuffer->setVertexInput(0, 1, &vertexInput); +cmdBuffer->draw(36); +cmdBuffer->endPass(); +``` + +[ **教程示例 07-Scissor Test** ](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/07-ScissorTest/Source/main.cpp) :通过裁剪测试只保留四分之一区域。 + +![image-20230916230138076](Resources/image-20230916230138076.png) + +### 模板测试(Stencil Test) + +通常情况下,我们使用模板测试是为了弥补裁剪测试只能使用矩形区域进行裁剪的缺陷,而模板测试允许我们自行制作一个模版(遮罩/Mask)来进行裁剪,就像是这样: + +![image-20230907200033700](Resources/image-20230907200033700-1694935064528-13.png) + +[模板测试 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/04 Advanced OpenGL/02 Stencil testing/) + +模版测试的核心机制可以看作是: + +- 对于 **具有模版附件** (缓冲区)的RenderTarget,才可以使用模版测试 +- 在`BeginPass`的时候,会 **清理模版缓冲区** (一般使用0) +- 我们可以在绑定流水线之后为其指定一个`StencilRef`(Int值),它可以当作是该流水线的ID +- 在绘制过程中,流水线会根据这个ID跟模版缓冲区上当前片段对应的模版值进行比较,我们可以 **设置比较方式** ,它决定了当前片段是否能通过模版测试,此外,我们还可以设置片段 通过模版测试 , 未通过模版测试 ,未通过深度测试时,怎么去处理模版缓冲区。 + +在QRhi中,假如我们想要制作一个模版,可能会写如下的代码: + +``` c++ +mMaskPipeline->setFlags(QRhiGraphicsPipeline::Flag::UsesStencilRef); //开启流水线的模版测试功能 +mMaskPipeline->setStencilTest(true); //开启模版测试 +QRhiGraphicsPipeline::StencilOpState maskStencilState; +maskStencilState.compareOp = QRhiGraphicsPipeline::CompareOp::Never; //该管线用于绘制模版(遮罩),我们不希望它在RT上绘制任何片段颜色,因此让它的片段永远不会通过模版测试 +maskStencilState.failOp = QRhiGraphicsPipeline::StencilOp::Replace; //设置当该片段没有通过模版测试时,使用StencilRef填充模版缓冲区 +mMaskPipeline->setStencilFront(maskStencilState); //指定正面的模版测试 +mMaskPipeline->setStencilBack(maskStencilState); //指定背面的模版测试 +mMaskPipeline->create(); +``` + +对于实际的图形,我们可能会使用这样的参数: + +``` c++ +mPipeline->setFlags(QRhiGraphicsPipeline::Flag::UsesStencilRef); //开启流水线的模版测试功能 +mPipeline->setStencilTest(true); //开启模版测试 +QRhiGraphicsPipeline::StencilOpState stencilState; +stencilState.compareOp = QRhiGraphicsPipeline::CompareOp::Equal; //我们希望在当前管线的StencilRef等于模版缓冲区上的片段值时才通过模版测试 +stencilState.passOp = QRhiGraphicsPipeline::StencilOp::Keep; //在通过测试后不会对模版缓冲区进行赋值 +stencilState.failOp = QRhiGraphicsPipeline::StencilOp::Keep; //在没有通过测试时也不会对模版缓冲区进行赋值 +mPipeline->setStencilFront(stencilState); +mPipeline->setStencilBack(stencilState); +mPipeline->create(); +``` + +绘制过程可能像是这样: + +``` c++ +const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); +const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; //使用 0 清理模版缓冲区 +cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, nullptr); + +cmdBuffer->setGraphicsPipeline(mMaskPipeline.get()); +cmdBuffer->setStencilRef(1); //设置StencilRef为1,该管线会在模版缓冲区上填充一块值为1的三角形区域 +cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); +cmdBuffer->setShaderResources(); +const QRhiCommandBuffer::VertexInput maskVertexInput(mMaskVertexBuffer.get(), 0); +cmdBuffer->setVertexInput(0, 1, &maskVertexInput); +cmdBuffer->draw(3); + +cmdBuffer->setGraphicsPipeline(mPipeline.get()); +cmdBuffer->setStencilRef(1); //设置StencilRef为1,该管线会用1跟对应位置的片段模版值进行比较,相等时才会通过模版测试,也就是会将片段绘制当颜色附件上 +cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); +cmdBuffer->setShaderResources(); +const QRhiCommandBuffer::VertexInput vertexInput(mVertexBuffer.get(), 0); +cmdBuffer->setVertexInput(0, 1, &vertexInput); +cmdBuffer->draw(3); + +cmdBuffer->endPass(); +``` + +[ **教程示例 08-StencilTest** ](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/08-StencilTest/Source/main.cpp) :使用MaskPipeline在模版缓冲区上绘制了一块值为1的三角形区域,之后绘制一个翻转的三角形,设置流水线的StencilRef为1,且只有跟模版缓冲区片段的对应值相等时才会通过模版测试,具体表现处理的效果就是只绘制两个三角形的重叠区域,即菱形。 + +![image-20230916230200246](Resources/image-20230916230200246.png) + +### 深度测试(Depth Test) + +开启深度测试,是为了让图像能够呈现出前后遮挡的关系,如果没有深度测试,就会出现错误的遮挡关系,就像是这样: + +![image-20230907201647233](Resources/image-20230907201647233-1694935064529-14.png) + +关于深度测试,这里有一篇非常好的文章: + +- [深度测试 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/04 Advanced OpenGL/01 Depth testing/) + +此外,在使用深度测试时,一般需要注意几个问题: + +- **是否需要开启深度写入和测试?** + - 开启深度写入和测试是为了让图形在三维空间上具有一定的遮挡关系,但并非三维空间中的所有图形都需要开启深度写入和测试,这其中一个特例就是半透明物体的渲染,大部分引擎会在渲染场景元素的时候,将不透明物体和透明物体进行排序,先开启深度测试和写入,绘制完所有的不透明物体,之后再关闭深度写入,保留深度测试,完成透明物体的渲染,详见:[一篇文章能不能说清楚半透明渲染 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/579419607) + +- **不同图形API之前的深度值范围并不一致!** + - OpenGL的深度值范围是`[-1,1]`,DX,Vulkan,Metal的深度值范围是`[0,1]`,在QRhi中,可以使用OpenGL的标准,只需要在上传MVP矩阵时,乘上`clipSpaceCorrMatrix` + +- **明确RenderTarget是否具有深度附件以及它的精度是多少** + - 具有深度附件的RenderTarget才能进行深度测试,通常情况下,窗口交换链中使用的深度模板附件是24位用于存储深度值,8位用于存储模板值,这也就代表了深度附件只能存储一定精度的深度值,假如两个平面的距离小于深度附件的最小精度,当进行深度测试的时候,两个平面得到的深度值是相同的,因此我们无法根据数据来明确 重叠像素之间的遮挡顺序 ,这个时候绘制顺序就决定了测试的结果, 又因为图形渲染管线是并行的,图形之间的执行顺序也无法保证,就会导致两个图形交替闪烁的现象,我们称之为 **深度冲突(Z-fighting)** 现象 + +- **如何避免深度冲突?** + - 增加深度附件的精度,比如去除模板位,扩充深度到32位 + - 错开图形的位置,使用 [深度偏差(Depth Bias) ](https://learn.microsoft.com/zh-cn/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage-depth-bias?redirectedfrom=MSDN) + +在QRhi中可以在创建 **QRhiGraphicsPipeline** 时,设置深度测试的相关参数: + +``` c++ +mPipeline->setDepthTest(true); +mPipeline->setDepthWrite(true); +mPipeline->setDepthOp(QRhiGraphicsPipeline::CompareOp::Less); +``` + +此外,还需注意RenderPass中使用的ClearValue: + +``` c++ +const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; +cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue); +``` + +> 通常情况下,我们会使用`1.0f`清理深度缓冲区,并使用`CompareOp::Less`来达到空间遮挡的效果。 + +**[教程示例 09-Depth Test](https://github.com/Italink/ModernGraphicsEngineGuide/tree/main/Source/1-GraphicsAPI/09-DepthTest/Source)** :绘制了一个平面,一个立方体,开启深度测试,使立方体的下半部分被正确遮挡。 + +![image-20230916230229915](Resources/image-20230916230229915.png) + +### 混合(Blending) + +混合通常用于半透明渲染,它本质上是一种处理前后重叠片段的机制。 + +我们称前一(已绘制在缓冲区上)片段为 **目标(Dst)片段** ,称 后一(待绘制)片段为 **源(Src)片段** ,混合主要是设定一些公式参数来定制这个过程(方括号`[]` 中的参数是可设置的): +$$ +\begin{align*} +& ResultColor_{rgb} = [SrcFactor_{alpha}]*SrcColor_{rgb}\ \ [Op]\ \ [DstFactor_{rgb}] * DstColor_{rgb} \\ +& ResultAlpha = [SrcFactor_alpha ]*SrcAlpha\ \ [Op]\ \ [DstFactor_{alpha}] * DstAlpha +\end{align*} +$$ +还有就是决定哪些颜色通道(RGBA)可以被写入。 + +其中Factor的值一般可以是: + +```C++ +Zero, +One, +SrcColor, +OneMinusSrcColor, +DstColor, +OneMinusDstColor, +SrcAlpha, +OneMinusSrcAlpha, +DstAlpha, +OneMinusDstAlpha, +ConstantColor, +OneMinusConstantColor, +ConstantAlpha, +OneMinusConstantAlpha, +SrcAlphaSaturate, +Src1Color, +OneMinusSrc1Color, +Src1Alpha, +OneMinusSrc1Alpha +``` + +Op的值可以是: + +``` c++ +Add, +Subtract, +ReverseSubtract, +Min, +Max +``` + +详见:[混合 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/04 Advanced OpenGL/03 Blending/) + +在 QRhi 中,可以在创建图形渲染管线时,设置混合的主要参数如下: + +``` C++ +class QRhiGraphicsPipeline{ +public: + struct TargetBlend { + ColorMask colorWrite = ColorMask(0xF); // R | G | B | A + bool enable = false; // 是否开启混合 + BlendFactor srcColor = One; // 源片段的RGB值 + BlendFactor dstColor = OneMinusSrcAlpha; // 目标片段的RGB值 + BlendOp opColor = Add; // 片段RGB的算术运算符 + BlendFactor srcAlpha = One; // 源片段的Alpha值 + BlendFactor dstAlpha = OneMinusSrcAlpha; // 目标片段的Alpha值 + BlendOp opAlpha = Add; // 片段Alpha的算术运算符 + }; +}; +``` + +使用也很简单: + +``` C++ +QRhiGraphicsPipeline::TargetBlend blend; +blend.enable = true; +blend.srcColor = QRhiGraphicsPipeline::SrcAlpha; +blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha; + +mPipeline->setTargetBlends({ blend }); +``` + +你可能注意到了这里设置的是 TargetBlend **s** ,是因为混合的配置跟RenderTarget的颜色附件一一对应,如果你的RT有多个颜色附件,那么你就需要设置多个TargetBlend。 + +**[教程示例 10-Blending](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/10-Blend/Source/main.cpp)** :绘制了一个平面,一个半透明立方体,开启混合,使半透明的立方体能正确显示。![image-20230916230258853](Resources/image-20230916230258853.png) + +## 面剔除(Face Culling) + +大多数时候我们在一个3D空间里,往往只能看到一个几何形体的单个侧面,比如一个球体,我们只能看到它的外表面,由于GPU只是一个并行数据处理器,没有空间遮挡的概念,对于球体被遮挡的部分,依旧会执行绘制。 + +为了解决这个问题,图形API提供了一个面剔除的功能,它要求几何形体的 顶点索引 按照一定 **缠绕顺序** 进行排序,通过缠绕顺序来确定三角形的正反面归属,这样就可以使用面剔除可以让图形渲染管线剔除掉背面的三角形。 + +详见:[面剔除 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/04 Advanced OpenGL/04 Face culling/) + +在QRhi中,使用面剔除要在创建图形渲染管线时,指定 **正面的评定方式** : + +``` c++ +mPipeline->setFrontFace(frontFace); +``` + +并设置 **剔除模式(CullMode)** : + +``` c++ +mPipeline->setCullMode(cullMode) +``` + +它们可以是如下的值: + +``` C++ +class QRhiGraphicsPipeline{ + enum CullMode { + None, //不剔除 + Front, //剔除正面 + Back //剔除背面 + }; + enum FrontFace { + CCW, //逆时针 + CW //顺时针 + }; +}; +``` + +[ **教程示例 11-FaceCulling** ](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/11-FaceCulling/Source/main.cpp) :绘制一个绕Y轴旋转的三角形,开启背面剔除。 + +![sdfag](Resources/sdfag.gif) + +## 线条宽度(Line Width) + +**部分** 图形API和平台支持在流水线中设置 线 图元 的宽度,在QRhi中使用如下接口即可: + +``` C++ + void QRhiGraphicsPipeline::setLineWidth(float width); +``` + +如果要查看当前后端是否支持设置线宽,请使用如下接口: + +``` C++ +bool isSupported = mRhi->isFeatureSupported(QRhi::Feature::WideLines); +``` + +笔者强烈建议不要使用线条图元,一方面是因为只有Vulkan和OpenGL在部分平台上支持线宽的设置,另一方面是因为图形API并不会处理连续线条的 **衔接(Join)** ,如果我们要图形渲染管线的`Line图元`绘制连续线条,那么将得到如下的效果: + +![image-20230911203532741](Resources/image-20230911203532741-1694935064529-15.png) + +在游戏引擎中,我们大多时候会使用三角形图元来绘制,自行计算线条的衔接部分: + +![image-20230911203659527](Resources/image-20230911203659527-1694935064529-16.png) + +> [15.6.1 线连接 (bluevoid.com)](http://www.bluevoid.com/opengl/sig00/advanced00/notes/node290.html) + +在本教程的后续章节中,会对连续线条的绘制做一些介绍。 + +## 实例化(Instancing) + +谈到实例化,笔者有一个问题想问下大家:你是否对GPU的算力有所了解? + +如果没有,笔者建议最好去找一些相关资料了解一下,在这里笔者简单列一个模糊的说明: + +- 常见电脑的显示屏分辨率一般是`1920X1080`,这也就意味着一帧图像大概有 **两百万的像素数据** ,如果是绘制一个全屏的矩形区域,当下的中端显卡(1050Ti以上),在 **关闭垂直同步** 之后一般能有 **几千的帧率(FPS)** + +由此可见,GPU的算力是非常恐怖的,但是在实际的渲染工程中,复杂场景的帧率一般都不高,这往往不是因为场景的几何复杂度降低了GPU的性能,而是一个非常重要的因素极大程度地制约了GPU,那就是CPU和GPU之间的数据传输带宽,而上面的描述中,GPU之所以能表现得这么强劲,正是因为顶点数据在一开始就上传到了GPU,这样之后的整个绘制过程都没有CPU和GPU的数据通信,而为了能够更好地发挥GPU的性能,我们一般会尽可能地减少CPU和GPU的数据传输,在之前的章节中,我们使用IndexBuffer来去除重复的顶点,正是基于这个目的,而 **实例化(Instancing)** ,则是图形API提供的一个高级特性,它使得我们可以使用 大量实例数据 和 公共数据 进行组合,来进行大批量的相似物体绘制。 + +这里有一个更加详细的教程: + +- [实例化 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/10%20Instancing/) + +在QRhi中,我们只需要在创建流水线时,在顶点输入布局中指定实例化数据: + +``` C++ +QRhiVertexInputLayout inputLayout; +inputLayout.setBindings({ + QRhiVertexInputBinding(2 * sizeof(float)), + QRhiVertexInputBinding(2 * sizeof(float), QRhiVertexInputBinding::PerInstance), //声明逐实例数据 +}); +mPipeline->setVertexInputLayout(inputLayout); +``` + +之后在绘制时,只需在draw函数中填入实例数量: + +``` c++ +void QRhiCommandBuffer::draw(quint32 vertexCount, + quint32 instanceCount = 1, + quint32 firstVertex = 0, + quint32 firstInstance = 0); + +void QRhiCommandBuffer::drawIndexed(quint32 indexCount, + quint32 instanceCount = 1, + quint32 firstIndex = 0, + qint32 vertexOffset = 0, + quint32 firstInstance = 0); +``` + +[ **教程示例 12-Instancing** ](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/12-Instancing/Source/main.cpp) :通过一个三角形的顶点数据 结合 一些 用于偏移数据的实例数据 来绘制大量三角形 。 + +![image-20230916235432483](Resources/image-20230916235432483.png) + +## 多渲染目标(Multi Render Target) + +在之前的章节中,我们都是使用的SwapChain提供的RenderTarget,在实际的渲染开发过程中,我们有时候会需要一个中间的渲染过程,为了存储这个过程的结果,我们会自己创建一个RT。 + +在QRhi中,创建一个RenderTarget是一件非常简单的事,就像是这样: + +``` c++ +mColorAttachment0.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(100, 100), 1, QRhiTexture::Flag::RenderTarget | QRhiTexture::UsedAsTransferSource)); +mColorAttachment0->create(); //创建颜色附件0 +mColorAttachment1.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(100, 100), 1, QRhiTexture::Flag::RenderTarget | QRhiTexture::UsedAsTransferSource)); +mColorAttachment1->create(); //创建颜色附件1 +mDepthStencilBuffer.reset(mRhi->newRenderBuffer(QRhiRenderBuffer::Type::DepthStencil, QSize(100, 100), 1, QRhiRenderBuffer::Flag(), QRhiTexture::Format::D24S8)); +mDepthStencilBuffer->create(); //创建深度(24位)模版(8位)附件 + +QRhiTextureRenderTargetDescription rtDesc; +rtDesc.setColorAttachments({ mColorAttachment0.get(),mColorAttachment1.get() }); +rtDesc.setDepthStencilBuffer(mDepthStencilBuffer.get()); +mRenderTarget.reset(mRhi->newTextureRenderTarget(rtDesc)); + +//根据RenderTarget的结构来创建RenderPass描述,在使用GraphicsPipeline时,必须指定RenderPassDesc +//这是因为流水线在创建时就需要明确它被用于何种结构的RenderPass,这里的结构指的是:RenderTarget的附件数量和格式 +mRenderPassDesc.reset(mRenderTarget->newCompatibleRenderPassDescriptor()); +mRenderTarget->setRenderPassDescriptor(mRenderPassDesc.get()); + +mRenderTarget->create(); +``` + +> 需要注意的是, **QRhiRenderBuffer** 可以简单当作是一种CPU不可读写的Texture,它具有比Texture更好的GPU处理性能,因此当我们不需要读写RenderTarget中的缓冲区时,请优先考虑使用RenderBuffer作为附件,通常情况下,交换链中的颜色附件和深度模版附件正是使用的RenderBuffer。 + +而多渲染目标其实只是指一个RenderTarget中含有多个 **颜色附件(ColorAttachment)** + +在FragmentShader中,我们可以这样来控制片段输出: + +``` c++ +#version 440 +layout(location = 0) out vec4 fragColor0; //输出到颜色附件0 +layout(location = 1) out vec4 fragColor1; //输出到颜色附件1 +void main(){ + fragColor0 = vec4(1.0f,0.0f,0.0f,1.0f); + fragColor1 = vec4(0.0f,0.0f,1.0f,1.0f); +} +``` + +绘制时只需在`beginPass`中使用我们自己的RenderTarget: + +``` +const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); +const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; +cmdBuffer->beginPass(myRenderTarget.get(), clearColor, dsClearValue); +cmdBuffer->endPass(); +``` + +[ **教程示例 13-MultiRenderTarget** ](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/13-MultiRenderTarget/Source/main.cpp) :在一个具有两个颜色附件的RT上绘制不同的颜色,并用另一条管线将它的颜色附件交替绘制到交换链的RT上。 + +![sdfsag](Resources/sdfsag.gif) + +## 计算管线(Compute Pipeline) + +计算管线可以让我们利用GPU的能力去做一些通用的数据计算。 + +与图形渲染管线不同的是: + +- 计算管线只需要编写 Compute Shader +- 计算管线中可以对Usage标识带有 `StorageBuffer` 的资源(缓冲区和图像)进行读写 +- 计算管线使用`dispatch(int x, int y, int z)` 开始执行调度 +- 计算管线不同于渲染管线,无法使用纹理采样,而是使用`image2D`,通过函数`imageLoad`读取像素值, `imageStore()`进行存储 +- 计算着色器中可以对SSBO使用原子(Atomic)操作 + +这里有一个不错的文章有更详细的介绍: + +- [缪之灵 - 从零开始的 Vulkan(八):计算管线 ](https://zhuanlan.zhihu.com/p/624613836) +- [Reddit - 如何合理划分工作组的调度?](https://www.reddit.com/r/vulkan/comments/barcii/ballpark_dispatch_group_size_for_gpgpu/) + +在QRhi中,使用计算管线的流程如下: + +创建可被计算管线使用的资源: + +``` c++ +mStorageBuffer.reset(mRhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, sizeof(float))); //缓冲区被用作StorageBuffer +mStorageBuffer->create(); +mTexture.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(ImageWidth, ImageHeight), 1, QRhiTexture::UsedWithLoadStore)); //图像可被计算管线读取和存储 +mTexture->create(); +``` + +创建计算管线: + +``` c++ +mPipeline.reset(mRhi->newComputePipeline()); +mShaderBindings.reset(mRhi->newShaderResourceBindings()); +QShader cs = QRhiHelper::newShaderFromCode(mRhi.get(), QShader::ComputeStage, R"(#version 440 + layout(std140, binding = 0) buffer StorageBuffer{ + int counter; + }SSBO; + layout (binding = 1, rgba8) uniform image2D Tex; + + void main(){ + int currentCounter = atomicAdd(SSBO.counter,1); + //int currentCounter = SSBO.counter = SSBO.counter + 1; + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + imageStore(Tex,pos,vec4(sin(currentCounter/100.0f),0,0,1)); + } +)"); +Q_ASSERT(cs.isValid()); + +mShaderBindings->setBindings({ + //设置计算管线的资源绑定,Load代表可读,Store代表可写 + QRhiShaderResourceBinding::bufferLoadStore(0,QRhiShaderResourceBinding::ComputeStage,mStorageBuffer.get()), QRhiShaderResourceBinding::imageLoadStore(1,QRhiShaderResourceBinding::ComputeStage,mTexture.get(),0), +}); +mShaderBindings->create(); +mPipeline->setShaderResourceBindings(mShaderBindings.get()); +mPipeline->create(); +``` + +执行计算管线: + +``` c++ +cmdBuffer->beginComputePass(resourceUpdates); +cmdBuffer->setComputePipeline(mPipeline.get()); +cmdBuffer->setShaderResources(); +cmdBuffer->dispatch(ImageWidth, ImageHeight, 1); //根据图像大小划分工作组 +cmdBuffer->endComputePass(); +``` + +[ **教程示例 14-ComputePipeline** ](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/14-ComputePipeline/Source/main.cpp) :使用SSBO存储一个初始为0的int值,使用ComputeShader让其自增,并将自增的结果标准化到`[0,1]`,根据当前的工作单元的坐标写入到图像中,并绘制在交换链上,得到的效果是: + +![image-20230917150925266](Resources/image-20230917150925266.png) + +奇怪的是,每个计算单元中Counter+1,并写入到对于位置的像素,正常情况下显示出来的颜色应该有一定梯度的才对,这种效果就像是每个计算单元在填充图像时,它们的Counter都是一样的。 + +如果你对多线程有所了解的话,应该想到了解决这个问题,只需要使用原子变量,在GPU中,我们可以对SSBO使用原子操作。 + +切换ComputeShader中的注释: + +``` c++ +layout(std140, binding = 0) buffer StorageBuffer{ + int counter; +}SSBO; +layout (binding = 1, rgba8) uniform image2D Tex; +const int ImageSize = 64 * 64; +void main(){ + //int currentCounter = SSBO.counter = SSBO.counter + 1; + int currentCounter = atomicAdd(SSBO.counter,1); //use atomic operation + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + imageStore(Tex,pos,vec4(currentCounter/float(ImageSize),0,0,1)); +} +``` + +我们能看到如下效果,它的渐变梯度显示了计算管线的并行执行情况: + +![image-20230917150952823](Resources/image-20230917150952823.png) + +## 间接渲染(Indirect Rendering) + +**间接渲染(Indirect Rendering)** 是一个不得不提的现代图形API的高级特性,当下一些亮眼的引擎技术往往都有它的影子,比如动辄上百万数量在模拟的GPU粒子,UE5中的虚拟几何技术 Nanite...,那它的作用是什么呢? + +在实例化的小节中我们提到,制约GPU性能的限制往往来源于与CPU的数据通信,而 **IndexBuffer** 和 **Instancing** 也只是让我们能够复用一部分顶点数据,但这部分数据依旧得来自于CPU,而如果这部分数据是动态的,则可能需要每帧都提交,这样依旧会在数据传输阶段产生性能瓶颈,所以很多开发者想到了全GPU的流程,直接在GPU上 生成和处理顶点数据,并使用这些数据直接进行绘制,但要实现这样的效果有一个非常致命的问题,由于调用`draw`命令时,需要指定渲染参数(顶点数量,索引数量,实例数量...),而全GPU的流程将导致CPU不清楚GPU产生的数据,而将GPU的数据 **回读(Readback)** 到CPU是性能损耗非常严重的操作,为了解决这个问题, **间接渲染(Indirect Rendering)** 允许我们调用`drawIndirect`命令时,可以指定一个 Indirect Buffer,该Buffer存储了对应`draw`命令的渲染参数。 + +打个比方,原先我们可能会调用如下的`draw`命令: + +```C++ +void QRhiCommandBuffer::draw( + uint32 vertexCount, + uint32 instanceCount = 1, + uint32 firstVertex = 0, + uint32 firstInstance = 0); +``` + +现代图形API会提供一个对应的间接指令: + +``` c++ +void QRhiCommandBuffer::drawIndirect( + QRhiBuffer *indirectBuffer, + uint32 bufferOffset = 0 +); +``` + +我们只需要在indirectBuffer中按如下内存结构去填充我们的渲染参数即可: + +```c++ +struct IndirectBuffer{ + uint32 vertexCount; + uint32 instanceCount; + uint32 firstVertex; + uint32 firstInstance; +} +``` + +就这样一个很简单的功能,让我们打破了CPU和GPU数据通信的桎梏,从而能够通过 **GPU 驱动管线(GPU Driven Pipeline)** + +很可惜的是,QRhi并没有提供 **Indirect Rendering** 的支持,但它提供了非常完备的原生API扩展手段。 + +这里笔者以Vulkan为例,介绍如何在QRhi的几何管线中使用间接缓冲区。 + +首先,我们需要创建一个具有 `VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT` 用途标识的缓冲区,这里笔者通过一些Hack的手段做了一些封装: + +``` c++ +mIndirectDrawBuffer.reset(QRhiHelper::newVkBuffer(mRhi.get(), QRhiBuffer::Static, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, sizeof(DispatchStruct))); +mIndirectDrawBuffer->create(); +``` + +之后我们按常规手段去初始化IndirectDrawBuffer的内存: + +``` c++ +QRhiResourceUpdateBatch* resourceUpdates = nullptr; +if(mSigSubmit.ensure()){ + resourceUpdates = mRhi->nextResourceUpdateBatch(); //初始化间接缓冲区 + DispatchStruct dispatch; + dispatch.x = dispatch.y = dispatch.z = 1; + resourceUpdates->uploadStaticBuffer(mIndirectDrawBuffer.get(), 0, sizeof(DispatchStruct), &dispatch); + cmdBuffer->resourceUpdate(resourceUpdates); + mRhi->finish(); +} +``` + +```c++ +QVulkanInstance* vkInstance = vulkanInstance(); +QRhiVulkanNativeHandles* vkHandle = (QRhiVulkanNativeHandles*)mRhi->nativeHandles(); +QVulkanDeviceFunctions* vkDevFunc = vkInstance->deviceFunctions(vkHandle->dev); + +QRhiVulkanCommandBufferNativeHandles* cmdBufferHandles = (QRhiVulkanCommandBufferNativeHandles*)cmdBuffer->nativeHandles(); +QVkCommandBuffer* cbD = QRHI_RES(QVkCommandBuffer, cmdBuffer); +VkCommandBuffer vkCmdBuffer = cmdBufferHandles->commandBuffer; + +QVkComputePipeline* pipelineHandle = (QVkComputePipeline*)mPipeline.get(); +VkPipeline vkPipeline = pipelineHandle->pipeline; + +QRhiBuffer::NativeBuffer indirectBufferHandle = mIndirectDrawBuffer->nativeBuffer(); +VkBuffer vkIndirectBuffer = *(VkBuffer*)indirectBufferHandle.objects[0]; + +cmdBuffer->beginExternal(); //开始扩展,之后可输入原生API的指令 +vkDevFunc->vkCmdBindPipeline(vkCmdBuffer, VkPipelineBindPoint::VK_PIPELINE_BIND_POINT_COMPUTE, vkPipeline); +QRhiHelper::setShaderResources(mPipeline.get(), cmdBuffer, mShaderBindings.get()); //辅助函数,用于更新VK流水线的描述符集绑定 +vkDevFunc->vkCmdDispatchIndirect(vkCmdBuffer, vkIndirectBuffer, 0); + +cmdBuffer->endExternal(); +``` + +在QRhi中,我们可以自己创建原生图形API的渲染资源,然后在录入CmdBuffer时,在`beginExternal()`和`endExternal()`之间插入原生API的指令,对于了解OpenGL但不熟悉现代图形API的同学,需要了解以下概念: + +### 渲染流程 + +现代图形API没有Context的概念,而是通过CommandBuffer(CommandList)录入指令,最后统一由Queue提交到GPU上,在多线程的渲染过程中,保证CommandBuffer的录入顺序即可。 + +现代图形API的渲染流程基本如下: + +``` c++ +void onRenderTick(){ + if(needInitResource){ + // Create Buffer,Pipeline,Bindings,Texture,Sampler.... + } + + if(needUpdateResource){ + cmdBuffer->...(); + //upload static buffer, update dynamic buffer,upload texture + } + + cmdBuffer->beginPass(renderTarget,clearValue); + + cmdBuffer->setPipeline(); + cmdBuffer->setViewport(...); + cmdBuffer->setShaderResources(..); + cmdBuffer->setVertexInput(0, 1, &vertexInput); + cmdBuffer->draw(...); + + cmdBuffer->endPass() +} +``` + +需要注意的是,在`beginPass()`和`endPass()`之间,我们不可以对渲染资源进行操作,这个处理过程我们会放在`beginPass()`之前,在绘制多个物体时,整个过程看上去就像是: + +``` c++ +for(auto item: renderItem) + item->tryUpdateResource(); + +cmdBuffer->beginPass(renderTarget,clearValue); + +for(auto item: renderItem) + item->drawCommands(); + +cmdBuffer->endPass() +``` + +### 内存同步 + +现代图形API需要我们手动去管理资源的同步 + +对于两个连续的draw命令: + +``` c++ +RenderPass1(); //渲染通道1 +RenderPass2(); //渲染通道2 +``` + +很多初学的小伙伴会以为当渲染通道1执行完,才会开始执行渲染通道2,很显然这个想法是错误的,因为CPU和GPU的执行是不同步的,cmdBuffer只是负责录制指令,这里的顺序只是指令的录制顺序,当指令有序提交到GPU上,也并不代表渲染通道1执行完才会开始执行渲染通道2,渲染通道2的执行时机取决于GPU上是否有空闲的计算资源,这也意味着两个渲染通道会产生 **顺序启动,无序执行** 的效果,这种做法可以极大程度地提高GPU的使用效率,但同样我们也会面临一个问题,如果两个drawCommands存在依赖关系(比如前一次计算管线输出的缓冲区作为下一个渲染管线的间接缓冲区输入),这种渲染上的并行就会产生不符合我们预期的结果(具体的表现情况在不同平台上有所差异),为了解决这个问题,现代图形API要求我们使用内存(Buffer和Texture)时,需要设置一个 **流水线阶段(Stage)** 和 **许可(Access)** ,这两个参数代表了当前内存只能在`Stage`阶段执行被`许可(Access)`的操作,在执行某个操作时,如果它不满足阶段和许可的要求,则会阻塞等待,我们可以通过 **屏障(Barrier)** 来改变内存的阶段和许可。 + +以Vulkan为例,为了解决 `前一次计算管线输出的缓冲区作为下一个渲染管线的间接缓冲区输入` 同步问题,我们可能会写出这样的代码: + +```C++ +VkBufferMemoryBarrier barrier0; //内存屏障的参数结构体 +barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; +barrier.srcAccessMask = 0; //从 可传输写入 转换到 可被着色器写入 +barrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT; +barrier.buffer = IndirectBuffer; //指定缓冲区 +barrier.offset = 0; +barrier.pNext = nullptr; +barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = -1; +barrier.size = IndirectBuffer->size(); +vkCmdPipelineBarrier(cmdBuffer, //从 任意图形阶段 转变为 计算着色器处理阶段 + VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 0, nullptr, 1, &barrier0, 0, nullptr); + +dispatch(...); //使用计算管线填充间接缓冲区的值 + +VkBufferMemoryBarrier barrier1; +barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; +barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; //从 可被着色器写入 转换到 可被间接渲染指令读取 +barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT; +barrier.buffer = IndirectBuffer; //指定缓冲区 +barrier.offset = 0; +barrier.pNext = nullptr; +barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = -1; +barrier.size = IndirectBuffer->size(); +vkCmdPipelineBarrier(cmdBuffer, //从 计算着色器处理阶段 转变为 间接渲染处理阶段 + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, + 0, 0, nullptr, 1, &barrier1, 0, nullptr); + +drawIndirect(...) //执行间接渲染 +``` + +Vulkan中的一些相关结构如下: + +``` c++ +typedef enum VkPipelineStageFlagBits { + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT = 0x00000001, + VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT = 0x00000002, + VK_PIPELINE_STAGE_VERTEX_INPUT_BIT = 0x00000004, + VK_PIPELINE_STAGE_VERTEX_SHADER_BIT = 0x00000008, + VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT = 0x00000010, + VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT = 0x00000020, + VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT = 0x00000040, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT = 0x00000080, + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT = 0x00000100, + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT = 0x00000200, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT = 0x00000400, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT = 0x00000800, + VK_PIPELINE_STAGE_TRANSFER_BIT = 0x00001000, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT = 0x00002000, + VK_PIPELINE_STAGE_HOST_BIT = 0x00004000, + VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT = 0x00008000, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT = 0x00010000, + VK_PIPELINE_STAGE_NONE = 0, + //... +} VkPipelineStageFlagBits; + +typedef enum VkAccessFlagBits { + VK_ACCESS_INDIRECT_COMMAND_READ_BIT = 0x00000001, + VK_ACCESS_INDEX_READ_BIT = 0x00000002, + VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT = 0x00000004, + VK_ACCESS_UNIFORM_READ_BIT = 0x00000008, + VK_ACCESS_INPUT_ATTACHMENT_READ_BIT = 0x00000010, + VK_ACCESS_SHADER_READ_BIT = 0x00000020, + VK_ACCESS_SHADER_WRITE_BIT = 0x00000040, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT = 0x00000080, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT = 0x00000100, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT = 0x00000200, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT = 0x00000400, + VK_ACCESS_TRANSFER_READ_BIT = 0x00000800, + VK_ACCESS_TRANSFER_WRITE_BIT = 0x00001000, + VK_ACCESS_HOST_READ_BIT = 0x00002000, + VK_ACCESS_HOST_WRITE_BIT = 0x00004000, + //... +} VkAccessFlagBits; +``` + +在QRhi中,它会根据流水线和资源更新批次的依赖关系,自动添加屏障,所以在正常使用的过程中,我们无需关心同步的问题,但如果我们使用了原生API的扩展,那么就需要自己手动去管理资源同步,所以大家需要了解同步的概念:屏障(Barrier),栅栏(Fence),事件(Event) + +> QRhi中为了实现自动添加资源屏障的机制,延迟了指令的提交,比如我们调用了`cmdBuffer->draw()`,它并没有立即调用`vkCmdDraw()`,而是将指令参数记录到QRhi自身维护的CommandBuffer中,直到`QRhi::endFrame()`时才真正执行,这是因为QRhi会分析RenderPass期间的 **着色器资源绑定(ShaderResourceBindings)** ,从而给依赖的资源添加屏障,但RenderPass期间又不允许对资源进行操作,而延迟提交指令让这个机制成为了可能,在`beginExternal`时,QRhi会将当前已存储的指令参数立即提交给原生API,这样我们就能安全地使用原生API进行扩展,但需要注意的是,在`External`期间,因为QRhi延迟提交的特性,我们就不能再使用QRhi的接口。 + +关于上述描述的细节,请自行查阅资料和文档。 + +[ **教程示例 15-IndirenctDraw** ](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/15-IndirectDraw/Source/main.cpp) :使用间接渲染驱动计算管线让一个数进行自增,并回读到CPU打印显示。 + +## 离屏渲染 (Offscreen Rendering) + +离屏渲染是指无窗口渲染,即不显示任何窗口就能在后台绘制图像。 + +现代图形API一般会提供 **OffscreenSurface** 的支持,对于OpenGL,我们一般会创建一个不可见的窗口来初始化上下文环境,从而进行离屏渲染。 + +在QRhi中使用离屏渲染是一件非常容易的事情,我们在创建Rhi时,初始化参数的window可以填`nullptr` + +之后使用`QRhiVulkan::beginOffscreenFrame`就能开始绘制离屏帧: + +``` c++ +QRhiCommandBuffer* cmdBuffer; +if (rhi->beginOffscreenFrame(&cmdBuffer) != QRhi::FrameOpSuccess) + return 1; + +// do rendering + +rhi->endOffscreenFrame(); +``` + +**[教程示例 16-Offscreen](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/16-Offscreen/Source/main.cpp)** :使用离屏渲染绘制一个三角形,回读到CPU,并将图像保存到本地。 + +![image-20230917144540928](Resources/image-20230917144540928-1694935064527-2.png) + +## 抗锯齿(Anti-Aliasing) + +通过图形渲染管线绘制一个三角形,由于RenderTarget具有一定的存储精度,所以我们得到的图像往往会存在一定锯齿: + +image-20230917155511334 + +而 **抗锯齿(反走样)** 的目标,就是为了柔化图形边缘的锯齿,让图像变成这样: + +image-20230917155423725 + +关于抗锯齿,这里有一个更详细的文档: + +- :[抗锯齿- LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/11%20Anti%20Aliasing/) + +大部分图形API都支持 **多重采样抗锯齿(Multi Ssample Anti-Aliasing)** 的特性。 + +除了MSAA,还有一些其他的抗锯齿算法,感兴趣可以阅读: + +- [张亚坤 - 主流抗锯齿方案详解(一)MSAA](https://zhuanlan.zhihu.com/p/415087003) +- [张亚坤 - 主流抗锯齿方案详解(二)TAA](https://zhuanlan.zhihu.com/p/425233743) +- [张亚坤 - 主流抗锯齿方案详解(三)FXAA](https://zhuanlan.zhihu.com/p/431384101) + +在QRhi中,我们可以这样检查特性是否支持: + +``` c+= +rhi->isFeatureSupported(QRhi::Feature::MultisampleTexture) +``` + +如果想要让交换链支持MSAA,其关键代码如下: + +``` c++ +//创建DS附件时,指定采样数为8 +mDSBuffer.reset( mRhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil,QSize(),8,QRhiRenderBuffer::UsedWithSwapChainOnly)); +mSwapChain->setDepthStencil(mDSBuffer.get()); +mSwapChain->setSampleCount(8); //设置重采样数量为8 +mSwapChain->createOrResize(); +``` + +在创建流水线时,需要保证流水线的采样数和渲染目标的采样数一致: + +``` c++ +mPipeline->setSampleCount(mSwapChain->sampleCount()); +``` + +需要注意的是:开启多重采样的Texture是不可回读的 + +如果想要回读MSAATexture,我们需要在创建RenderTarget时,指定一个采样数为1的 **ResolveTexture** ,回读ResolveTexture而不是MSAATexture: + +```c++ +QScopedPointer msaaTexture; +QScopedPointer msaaResolveTexture; +QScopedPointer renderTargetTexture; +QScopedPointer renderTarget; +QScopedPointer renderTargetDesc; + +msaaTexture.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(1280, 720), 8, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); +msaaTexture->create(); +msaaResolveTexture.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(1280, 720), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); +msaaResolveTexture->create(); + +QRhiColorAttachment colorAttachment; +colorAttachment.setTexture(msaaTexture.get()); //设置msaa纹理 +colorAttachment.setResolveTexture(msaaResolveTexture.get()); //指定用于解析的纹理 + +renderTarget.reset(rhi->newTextureRenderTarget({ colorAttachment })); +renderTargetDesc.reset(renderTarget->newCompatibleRenderPassDescriptor()); +renderTarget->setRenderPassDescriptor(renderTargetDesc.get()); +renderTarget->create(); + +//... + +QRhiReadbackDescription rb(msaaResolveTexture.get()); //回读 msaaResolveTexture 而不是 msaaTexture +resourceUpdates->readBackTexture(rb, &rbResult); +cmdBuffer->endPass(resourceUpdates); +``` + +[ **教程示例 17-MSAA** ](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/1-GraphicsAPI/17-MSAA/Source/main.cpp) :使用离屏渲染绘制一个重采样数为8的三角形,回读到CPU,并将图像保存到本地。 + +![image-20230917154919968](Resources/image-20230917154919968.png) diff --git a/Docs/01-GraphicsAPI/Resources/118.gif b/Docs/01-GraphicsAPI/Resources/118.gif new file mode 100644 index 00000000..50ada7cf Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/118.gif differ diff --git a/Docs/01-GraphicsAPI/Resources/14568.gif b/Docs/01-GraphicsAPI/Resources/14568.gif new file mode 100644 index 00000000..03dc8602 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/14568.gif differ diff --git a/Docs/01-GraphicsAPI/Resources/1468.gif b/Docs/01-GraphicsAPI/Resources/1468.gif new file mode 100644 index 00000000..9f74e988 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/1468.gif differ diff --git a/Docs/01-GraphicsAPI/Resources/1df7ad4e15915530a451f053bbcfeea8.png b/Docs/01-GraphicsAPI/Resources/1df7ad4e15915530a451f053bbcfeea8.png new file mode 100644 index 00000000..d05cb1b6 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/1df7ad4e15915530a451f053bbcfeea8.png differ diff --git a/Docs/01-GraphicsAPI/Resources/21ae934529205b73ae6d048c89898edb.png b/Docs/01-GraphicsAPI/Resources/21ae934529205b73ae6d048c89898edb.png new file mode 100644 index 00000000..c5e221c3 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/21ae934529205b73ae6d048c89898edb.png differ diff --git a/Docs/01-GraphicsAPI/Resources/418.gif b/Docs/01-GraphicsAPI/Resources/418.gif new file mode 100644 index 00000000..fe1815de Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/418.gif differ diff --git a/Docs/01-GraphicsAPI/Resources/5e8862ed2174022bee08dd884f4300cf.png b/Docs/01-GraphicsAPI/Resources/5e8862ed2174022bee08dd884f4300cf.png new file mode 100644 index 00000000..e98dd60b Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/5e8862ed2174022bee08dd884f4300cf.png differ diff --git a/Docs/01-GraphicsAPI/Resources/905430d6d8ca6d241f51eab02af3be71232a0517.png@942w_683h_progressive-1694935064528-4.webp b/Docs/01-GraphicsAPI/Resources/905430d6d8ca6d241f51eab02af3be71232a0517.png@942w_683h_progressive-1694935064528-4.webp new file mode 100644 index 00000000..bb6da965 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/905430d6d8ca6d241f51eab02af3be71232a0517.png@942w_683h_progressive-1694935064528-4.webp differ diff --git a/Docs/01-GraphicsAPI/Resources/905430d6d8ca6d241f51eab02af3be71232a0517.png@942w_683h_progressive.webp b/Docs/01-GraphicsAPI/Resources/905430d6d8ca6d241f51eab02af3be71232a0517.png@942w_683h_progressive.webp new file mode 100644 index 00000000..bb6da965 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/905430d6d8ca6d241f51eab02af3be71232a0517.png@942w_683h_progressive.webp differ diff --git a/Docs/01-GraphicsAPI/Resources/96997598049549ea6f3691dd3f71d92b0c4646dd.png@942w_711h_progressive.webp b/Docs/01-GraphicsAPI/Resources/96997598049549ea6f3691dd3f71d92b0c4646dd.png@942w_711h_progressive.webp new file mode 100644 index 00000000..eeda637f Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/96997598049549ea6f3691dd3f71d92b0c4646dd.png@942w_711h_progressive.webp differ diff --git a/Docs/01-GraphicsAPI/Resources/9ef98d9520af66ac686cd86f70e85803.png b/Docs/01-GraphicsAPI/Resources/9ef98d9520af66ac686cd86f70e85803.png new file mode 100644 index 00000000..ad13e8d3 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/9ef98d9520af66ac686cd86f70e85803.png differ diff --git a/Docs/01-GraphicsAPI/Resources/GL-Pipeline.jpg b/Docs/01-GraphicsAPI/Resources/GL-Pipeline.jpg new file mode 100644 index 00000000..f1690d6c Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/GL-Pipeline.jpg differ diff --git a/Docs/01-GraphicsAPI/Resources/Parallax_scroll.gif b/Docs/01-GraphicsAPI/Resources/Parallax_scroll.gif new file mode 100644 index 00000000..6a6760bf Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/Parallax_scroll.gif differ diff --git a/Docs/01-GraphicsAPI/Resources/Screenshot_2022-04-27_092223_wjz58k.png b/Docs/01-GraphicsAPI/Resources/Screenshot_2022-04-27_092223_wjz58k.png new file mode 100644 index 00000000..cae78f50 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/Screenshot_2022-04-27_092223_wjz58k.png differ diff --git a/Docs/01-GraphicsAPI/Resources/aa579998d7db8ba46a9a96fe4c292232.png b/Docs/01-GraphicsAPI/Resources/aa579998d7db8ba46a9a96fe4c292232.png new file mode 100644 index 00000000..fad35390 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/aa579998d7db8ba46a9a96fe4c292232.png differ diff --git a/Docs/01-GraphicsAPI/Resources/anti_aliasing_rasterization.png b/Docs/01-GraphicsAPI/Resources/anti_aliasing_rasterization.png new file mode 100644 index 00000000..7ca46f0b Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/anti_aliasing_rasterization.png differ diff --git a/Docs/01-GraphicsAPI/Resources/coordinate_systems.png b/Docs/01-GraphicsAPI/Resources/coordinate_systems.png new file mode 100644 index 00000000..7a554a69 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/coordinate_systems.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230429172832427.png b/Docs/01-GraphicsAPI/Resources/image-20230429172832427.png new file mode 100644 index 00000000..dff5ad09 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230429172832427.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230429175841296.png b/Docs/01-GraphicsAPI/Resources/image-20230429175841296.png new file mode 100644 index 00000000..5d197ba1 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230429175841296.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230430003736149.png b/Docs/01-GraphicsAPI/Resources/image-20230430003736149.png new file mode 100644 index 00000000..a3c39ce6 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230430003736149.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230430010809312.png b/Docs/01-GraphicsAPI/Resources/image-20230430010809312.png new file mode 100644 index 00000000..96c36a24 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230430010809312.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230501111914434.png b/Docs/01-GraphicsAPI/Resources/image-20230501111914434.png new file mode 100644 index 00000000..c79af6fa Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230501111914434.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230501112954761.png b/Docs/01-GraphicsAPI/Resources/image-20230501112954761.png new file mode 100644 index 00000000..5482834f Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230501112954761.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230501131107153.png b/Docs/01-GraphicsAPI/Resources/image-20230501131107153.png new file mode 100644 index 00000000..215ecb33 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230501131107153.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230501131226503.png b/Docs/01-GraphicsAPI/Resources/image-20230501131226503.png new file mode 100644 index 00000000..b10f140c Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230501131226503.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230501131250421.png b/Docs/01-GraphicsAPI/Resources/image-20230501131250421.png new file mode 100644 index 00000000..656fc71b Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230501131250421.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230501131443750.png b/Docs/01-GraphicsAPI/Resources/image-20230501131443750.png new file mode 100644 index 00000000..4b52ace5 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230501131443750.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230501131634427.png b/Docs/01-GraphicsAPI/Resources/image-20230501131634427.png new file mode 100644 index 00000000..71149535 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230501131634427.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503153613484.png b/Docs/01-GraphicsAPI/Resources/image-20230503153613484.png new file mode 100644 index 00000000..b119db3c Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503153613484.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503153838795.png b/Docs/01-GraphicsAPI/Resources/image-20230503153838795.png new file mode 100644 index 00000000..e732a974 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503153838795.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503162819180.png b/Docs/01-GraphicsAPI/Resources/image-20230503162819180.png new file mode 100644 index 00000000..40946333 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503162819180.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503163338306.png b/Docs/01-GraphicsAPI/Resources/image-20230503163338306.png new file mode 100644 index 00000000..fa1474ac Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503163338306.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503191610995.png b/Docs/01-GraphicsAPI/Resources/image-20230503191610995.png new file mode 100644 index 00000000..1aa0a75e Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503191610995.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503194533809.png b/Docs/01-GraphicsAPI/Resources/image-20230503194533809.png new file mode 100644 index 00000000..524a5f1c Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503194533809.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503194825627.png b/Docs/01-GraphicsAPI/Resources/image-20230503194825627.png new file mode 100644 index 00000000..8bab66c9 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503194825627.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503201624943.png b/Docs/01-GraphicsAPI/Resources/image-20230503201624943.png new file mode 100644 index 00000000..6c65d8c4 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503201624943.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503201733475.png b/Docs/01-GraphicsAPI/Resources/image-20230503201733475.png new file mode 100644 index 00000000..e6cc9a28 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503201733475.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503205925995.png b/Docs/01-GraphicsAPI/Resources/image-20230503205925995.png new file mode 100644 index 00000000..76f1a1a3 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503205925995.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503214715725.png b/Docs/01-GraphicsAPI/Resources/image-20230503214715725.png new file mode 100644 index 00000000..cc26efca Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503214715725.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503214831931.png b/Docs/01-GraphicsAPI/Resources/image-20230503214831931.png new file mode 100644 index 00000000..16296ee7 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503214831931.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230503215214663.png b/Docs/01-GraphicsAPI/Resources/image-20230503215214663.png new file mode 100644 index 00000000..16296ee7 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230503215214663.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230520124250341.png b/Docs/01-GraphicsAPI/Resources/image-20230520124250341.png new file mode 100644 index 00000000..6dbaf539 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230520124250341.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230521082131102.png b/Docs/01-GraphicsAPI/Resources/image-20230521082131102.png new file mode 100644 index 00000000..0fc4d8d8 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230521082131102.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230521093653520.png b/Docs/01-GraphicsAPI/Resources/image-20230521093653520.png new file mode 100644 index 00000000..6fc62d75 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230521093653520.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230521093725199.png b/Docs/01-GraphicsAPI/Resources/image-20230521093725199.png new file mode 100644 index 00000000..2710161c Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230521093725199.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230521094519474.png b/Docs/01-GraphicsAPI/Resources/image-20230521094519474.png new file mode 100644 index 00000000..39cc84a4 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230521094519474.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230521100004771.png b/Docs/01-GraphicsAPI/Resources/image-20230521100004771.png new file mode 100644 index 00000000..238dc260 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230521100004771.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230521100350075.png b/Docs/01-GraphicsAPI/Resources/image-20230521100350075.png new file mode 100644 index 00000000..7e4d7700 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230521100350075.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230521104752631.png b/Docs/01-GraphicsAPI/Resources/image-20230521104752631.png new file mode 100644 index 00000000..00b61850 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230521104752631.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230527105811415.png b/Docs/01-GraphicsAPI/Resources/image-20230527105811415.png new file mode 100644 index 00000000..d65a8a70 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230527105811415.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230527223242963.png b/Docs/01-GraphicsAPI/Resources/image-20230527223242963.png new file mode 100644 index 00000000..33a37d32 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230527223242963.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230528104226972.png b/Docs/01-GraphicsAPI/Resources/image-20230528104226972.png new file mode 100644 index 00000000..fb6cd81c Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230528104226972.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230528152235000.png b/Docs/01-GraphicsAPI/Resources/image-20230528152235000.png new file mode 100644 index 00000000..c5878005 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230528152235000.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194119839-1694935064528-7.png b/Docs/01-GraphicsAPI/Resources/image-20230830194119839-1694935064528-7.png new file mode 100644 index 00000000..3370dc8d Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194119839-1694935064528-7.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194119839.png b/Docs/01-GraphicsAPI/Resources/image-20230830194119839.png new file mode 100644 index 00000000..3370dc8d Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194119839.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194150854-1694935064528-8.png b/Docs/01-GraphicsAPI/Resources/image-20230830194150854-1694935064528-8.png new file mode 100644 index 00000000..d81733e1 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194150854-1694935064528-8.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194150854.png b/Docs/01-GraphicsAPI/Resources/image-20230830194150854.png new file mode 100644 index 00000000..d81733e1 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194150854.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194235278-1694935064528-9.png b/Docs/01-GraphicsAPI/Resources/image-20230830194235278-1694935064528-9.png new file mode 100644 index 00000000..60c2f729 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194235278-1694935064528-9.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194235278.png b/Docs/01-GraphicsAPI/Resources/image-20230830194235278.png new file mode 100644 index 00000000..60c2f729 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194235278.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194606222.png b/Docs/01-GraphicsAPI/Resources/image-20230830194606222.png new file mode 100644 index 00000000..521eb6a1 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194606222.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194643278-1694935064528-10.png b/Docs/01-GraphicsAPI/Resources/image-20230830194643278-1694935064528-10.png new file mode 100644 index 00000000..b01819d3 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194643278-1694935064528-10.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194643278.png b/Docs/01-GraphicsAPI/Resources/image-20230830194643278.png new file mode 100644 index 00000000..b01819d3 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194643278.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194744480-1694935064528-11.png b/Docs/01-GraphicsAPI/Resources/image-20230830194744480-1694935064528-11.png new file mode 100644 index 00000000..087d07c6 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194744480-1694935064528-11.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830194744480.png b/Docs/01-GraphicsAPI/Resources/image-20230830194744480.png new file mode 100644 index 00000000..087d07c6 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830194744480.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230830195635251.png b/Docs/01-GraphicsAPI/Resources/image-20230830195635251.png new file mode 100644 index 00000000..087d07c6 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230830195635251.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230907194736072.png b/Docs/01-GraphicsAPI/Resources/image-20230907194736072.png new file mode 100644 index 00000000..bc2cdec7 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230907194736072.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230907194807029-1694935064528-12.png b/Docs/01-GraphicsAPI/Resources/image-20230907194807029-1694935064528-12.png new file mode 100644 index 00000000..395ae9b4 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230907194807029-1694935064528-12.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230907194807029.png b/Docs/01-GraphicsAPI/Resources/image-20230907194807029.png new file mode 100644 index 00000000..395ae9b4 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230907194807029.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230907200033700-1694935064528-13.png b/Docs/01-GraphicsAPI/Resources/image-20230907200033700-1694935064528-13.png new file mode 100644 index 00000000..4ce573a7 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230907200033700-1694935064528-13.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230907200033700.png b/Docs/01-GraphicsAPI/Resources/image-20230907200033700.png new file mode 100644 index 00000000..4ce573a7 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230907200033700.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230907201647233-1694935064529-14.png b/Docs/01-GraphicsAPI/Resources/image-20230907201647233-1694935064529-14.png new file mode 100644 index 00000000..88702ea0 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230907201647233-1694935064529-14.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230907201647233.png b/Docs/01-GraphicsAPI/Resources/image-20230907201647233.png new file mode 100644 index 00000000..88702ea0 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230907201647233.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230911202544320-1694935064528-5.png b/Docs/01-GraphicsAPI/Resources/image-20230911202544320-1694935064528-5.png new file mode 100644 index 00000000..a3dc4261 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230911202544320-1694935064528-5.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230911202544320.png b/Docs/01-GraphicsAPI/Resources/image-20230911202544320.png new file mode 100644 index 00000000..a3dc4261 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230911202544320.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230911202700715-1694935064528-6.png b/Docs/01-GraphicsAPI/Resources/image-20230911202700715-1694935064528-6.png new file mode 100644 index 00000000..42f3ef03 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230911202700715-1694935064528-6.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230911202700715.png b/Docs/01-GraphicsAPI/Resources/image-20230911202700715.png new file mode 100644 index 00000000..42f3ef03 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230911202700715.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230911203532741-1694935064529-15.png b/Docs/01-GraphicsAPI/Resources/image-20230911203532741-1694935064529-15.png new file mode 100644 index 00000000..855e5355 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230911203532741-1694935064529-15.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230911203532741.png b/Docs/01-GraphicsAPI/Resources/image-20230911203532741.png new file mode 100644 index 00000000..855e5355 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230911203532741.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230911203659527-1694935064529-16.png b/Docs/01-GraphicsAPI/Resources/image-20230911203659527-1694935064529-16.png new file mode 100644 index 00000000..1e0c874d Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230911203659527-1694935064529-16.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230911203659527.png b/Docs/01-GraphicsAPI/Resources/image-20230911203659527.png new file mode 100644 index 00000000..1e0c874d Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230911203659527.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230916230138076.png b/Docs/01-GraphicsAPI/Resources/image-20230916230138076.png new file mode 100644 index 00000000..24fddfc7 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230916230138076.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230916230200246.png b/Docs/01-GraphicsAPI/Resources/image-20230916230200246.png new file mode 100644 index 00000000..baf10387 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230916230200246.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230916230229915.png b/Docs/01-GraphicsAPI/Resources/image-20230916230229915.png new file mode 100644 index 00000000..c127eca4 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230916230229915.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230916230258853.png b/Docs/01-GraphicsAPI/Resources/image-20230916230258853.png new file mode 100644 index 00000000..c650d803 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230916230258853.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230916235432483.png b/Docs/01-GraphicsAPI/Resources/image-20230916235432483.png new file mode 100644 index 00000000..5ad17155 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230916235432483.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230917144540928-1694935064527-2.png b/Docs/01-GraphicsAPI/Resources/image-20230917144540928-1694935064527-2.png new file mode 100644 index 00000000..4e8c61d8 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230917144540928-1694935064527-2.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230917144540928.png b/Docs/01-GraphicsAPI/Resources/image-20230917144540928.png new file mode 100644 index 00000000..4e8c61d8 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230917144540928.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230917150925266.png b/Docs/01-GraphicsAPI/Resources/image-20230917150925266.png new file mode 100644 index 00000000..819614a0 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230917150925266.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230917150952823.png b/Docs/01-GraphicsAPI/Resources/image-20230917150952823.png new file mode 100644 index 00000000..f4dd9841 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230917150952823.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230917154919968.png b/Docs/01-GraphicsAPI/Resources/image-20230917154919968.png new file mode 100644 index 00000000..fcce7e9c Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230917154919968.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230917155331641.png b/Docs/01-GraphicsAPI/Resources/image-20230917155331641.png new file mode 100644 index 00000000..2db4901f Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230917155331641.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230917155423725.png b/Docs/01-GraphicsAPI/Resources/image-20230917155423725.png new file mode 100644 index 00000000..b126af40 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230917155423725.png differ diff --git a/Docs/01-GraphicsAPI/Resources/image-20230917155511334.png b/Docs/01-GraphicsAPI/Resources/image-20230917155511334.png new file mode 100644 index 00000000..a27390ce Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/image-20230917155511334.png differ diff --git a/Docs/01-GraphicsAPI/Resources/mipmaps.png b/Docs/01-GraphicsAPI/Resources/mipmaps.png new file mode 100644 index 00000000..d46b6d3a Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/mipmaps.png differ diff --git a/Docs/01-GraphicsAPI/Resources/sdfag.gif b/Docs/01-GraphicsAPI/Resources/sdfag.gif new file mode 100644 index 00000000..5d229047 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/sdfag.gif differ diff --git a/Docs/01-GraphicsAPI/Resources/sdfsag.gif b/Docs/01-GraphicsAPI/Resources/sdfsag.gif new file mode 100644 index 00000000..6753d785 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/sdfsag.gif differ diff --git a/Docs/01-GraphicsAPI/Resources/shaderconditioning.pngwidth=1280&name=shaderconditioning-1682759360241-2.png b/Docs/01-GraphicsAPI/Resources/shaderconditioning.pngwidth=1280&name=shaderconditioning-1682759360241-2.png new file mode 100644 index 00000000..779dd533 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/shaderconditioning.pngwidth=1280&name=shaderconditioning-1682759360241-2.png differ diff --git a/Docs/01-GraphicsAPI/Resources/tex_coords.png b/Docs/01-GraphicsAPI/Resources/tex_coords.png new file mode 100644 index 00000000..d1545df7 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/tex_coords.png differ diff --git a/Docs/01-GraphicsAPI/Resources/v2-b8badd685b39f310d5420e95c1283995_1440w.webp b/Docs/01-GraphicsAPI/Resources/v2-b8badd685b39f310d5420e95c1283995_1440w.webp new file mode 100644 index 00000000..1e081f93 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/v2-b8badd685b39f310d5420e95c1283995_1440w.webp differ diff --git a/Docs/01-GraphicsAPI/Resources/viewing-frustum-_frustum.fit_lim.size_768x.gif b/Docs/01-GraphicsAPI/Resources/viewing-frustum-_frustum.fit_lim.size_768x.gif new file mode 100644 index 00000000..bdb7fe23 Binary files /dev/null and b/Docs/01-GraphicsAPI/Resources/viewing-frustum-_frustum.fit_lim.size_768x.gif differ diff --git "a/Docs/02-EngineTechnology/0.\346\270\262\346\237\223\346\236\266\346\236\204.md" "b/Docs/02-EngineTechnology/0.\346\270\262\346\237\223\346\236\266\346\236\204.md" new file mode 100644 index 00000000..682d95af --- /dev/null +++ "b/Docs/02-EngineTechnology/0.\346\270\262\346\237\223\346\236\266\346\236\204.md" @@ -0,0 +1,821 @@ +# 渲染架构 + +在之前的章节中,我们学会了如何使用 **QRhi** 完成图形的绘制,但图形渲染并非只是使用图形API那么简单,随着3D场景中图形复杂度的提升,直接使用RHI的接口编写 线性的逻辑 会让代码变得非常混乱,而为了让代码的组织结构更为合理,现代图形引擎都会在 RHI 的基础上,进行进一步的封装。 + +比如我们可以将流水线的执行封装到 **渲染项(RenderItem)** 里面,那整个渲染过程可能会是这样: + +```C++ +void onRenderTick(QRhiCommandBuffer* cmdBuffer){ + QRhiResourceUpdateBatch* batch = rhi->nextResourceUpdateBatch(); + + for(auto item: renderItems){ + item->tryUpload(batch); + item->updated(batch); + } + + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, batch); + + for(auto item: renderItems) + item->draw(cmdBuffer); + + cmdBuffer->endPass(); +} +``` + +这样我们就能够 很轻松组织 代码结构 来将 许多几何图形 绘制到 一张图像 上: + +![image-20231006120654050](Resources/image-20231006120654050.png) + +这个 **渲染过程(RenderPass)** 我们一般称之为 **BasePass** / **MeshPass** / **PrimitivePass** 。 + +该Pass的职责就是绘制3D场景中几乎全部的几何形体。 + +但仅仅通过MeshPass ,我们得到的图形效果往往是比较朴素的,虽然我们可以调整 Pass内部 渲染项 的 流水线着色逻辑 来丰富 图形效果(前向渲染),但在现代图形渲染中,我们会进行多帧(多Pass)的处理 来调整 整个画面 的效果(延迟渲染 + 后处理)。 + +这么说可能有点难理解,但看完下面的图示,相信你就能明白了: + +![image-20231006114116772](Resources/image-20231006114116772.png) + +![image-20231006123724423](Resources/image-20231006123724423.png) + +![image-20231006124210852](Resources/image-20231006124210852.png) + +![image-20231006125745195](Resources/image-20231006125745195.png) + +![image-20231006131116388](Resources/image-20231006131116388.png) + +![image-20231006130046879](Resources/image-20231006130046879.png) + +为了能够更好地支持这种多Pass的渲染逻辑,现代图形引擎也都封装了相应的结构,通常情况下,我们称其为 **Render Graph / Frame Graph / Scene Graph** + +## RenderGraph + +关于RenderGraph的详细描述,有很多优秀的文章,强烈建议大家去阅读一下: + +- [不知名书杯 | 理解 Frame Graph](https://zhuanlan.zhihu.com/p/639001043) +- [向往 | 剖析虚幻渲染体系(11)- RDG](https://www.cnblogs.com/timlly/p/15217090.html) +- [丛越 | 游戏引擎随笔 0x03:可扩展的渲染管线架构](https://zhuanlan.zhihu.com/p/70668533) +- [Ubp.a | FrameGraph|设计&基于DX12实现](https://zhuanlan.zhihu.com/p/147207161) + +可参考的实现主要有: + +- [Unreal Engine | Render Dependency Graph](https://docs.unrealengine.com/5.3/en-US/render-dependency-graph-in-unreal-engine/) +- [Unity | Render Graph System](https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@17.0/manual/render-graph-system.html) +- [O3DE | Pass System](https://docs.o3de.org/docs/atom-guide/dev-guide/passes/pass-system/) +- [Qt Quick | Scene Graph](https://doc.qt.io/qt-6/qtquick-visualcanvas-scenegraph.html) + +笔者在该教程的代码中实现了一个简易的 RenderGraph ,下文主要以伪代码的形式,谈一下笔者的 设计思路 和 开发过程 遇到的问题 + +> 学习别人积淀的成果是一个非常大的挑战,因为我们没有遭遇过与开发者相同的困境,所以很难 设身处地 的思考,为什么要这么做~ + +RenderGraph的首要设计目标,是为了能够完成 渲染依赖图 的构建: + +![image-20231006140150745](Resources/image-20231006140150745.png) + +假如想实现这样的渲染结构,在代码层面,你会如何设计呢? + +笔者首先对 **RenderPassNode** 进行了抽象: + +``` c++ +class IRenderPassNode{ + virtual void onRecrecteResource() = 0; //在此处创建RenderPass所需的RHI资源 + virutal void onRenderTick(QRhiCommandBuffer*) = 0; //执行渲染逻辑 + + virtual QVariant setupInputParam(QString) = 0; //由外部填充该Pass的输入参数 + virtual QVariant getOutputParam(QString) = 0; //由外部获取该Pass的输出参数 +protected: + QMap mInputs; //输入参数 + QMap mOutputs; //输出参数 +} +``` + +在此基础上, **MeshPass** 的逻辑大概变成了这样: + +``` c++ +class QMeshPassNode: public IRenderPassNode { +public: + void addRenderItem(...); + void removeRenderItem(...); +public: + QVector mRenderItems; + QRhiTextureRenderTarget mRenderTarget; +private: + void onRecrecteResource() { + createRenderTarger(mRenderTarget); + mOutput["BaseColor"] = mRenderTarget.ColorAttachment0; //注册输出参数 + + for(auto item: mRenderItems){ //创建所有Item的渲染资源 + item->createResources(); + } + } + + void onRenderTick(QRhiCommandBuffer* cmdBuffer) override { + QRhiResourceUpdateBatch* batch = rhi->nextResourceUpdateBatch(); + + for(auto item: mRenderItems){ //上传和更新所有Item的资源 + item->tryUpload(batch); + item->updated(batch); + } + + cmdBuffer->beginPass(mRenderTarget, clearColor, dsClearValue, batch); + + for(auto item: mRenderItems) //执行Item的绘制 + item->draw(cmdBuffer); + + cmdBuffer->endPass(); + } +} +``` + +再加一点其他的润色,笔者完成了第一个版本的 FrameGraph 结构: + +``` c++ +widget.setFrameGraph( + QFrameGraph::Begin() + .addPass( + QMeshRenderPass::Create("MeshPass") //创建MeshPass + .addComponent( //添加静态网格体组件 + QStaticMeshRenderComponent::Create("StaticMesh") + .setStaticMesh(QStaticMesh::CreateFromFile(RESOURCE_DIR"/Model/mandalorian_ship/scene.gltf")) + .setRotation(QVector3D(-90, 0, 0)) + ) + ) + .addPass( + QPixelFilterRenderPass::Create("BrightPixels") //该Pass用于过滤图像中亮度值超过1.0 + .setTextureIn_Src("MeshPass",QMeshRenderPass::Out::BaseColor) //设置该Pass的输入为MeshPass输出的BaseColor + .setFilterCode(R"( + const float threshold = 1.0f; + void main() { + vec4 color = texture(uTexture, vUV); + float value = max(max(color.r,color.g),color.b); + outFragColor = (1-step(value, threshold)) * color * 100; + } + )") + ) + .addPass( + QBlurRenderPass::Create("Blur") //该Pass用于模糊图像 + .setBlurIterations(1) + .setTextureIn_Src("BrightPixels", QPixelFilterRenderPass::Out::Result) //设置该Pass的输入为BrightPixels的输出结果 + ) + .addPass( + QBloomRenderPass::Create("Bloom") //该Pass用于混合原图像和泛光的图像 + .setTextureIn_Raw("MeshPass", QMeshRenderPass::Out::BaseColor) + .setTextureIn_Blur("Blur", QBlurRenderPass::Out::Result) + ) + .addPass( + QToneMappingRenderPass::Create("ToneMapping") //该Pass对图像进行色调映射 + .setTextureIn_Src("Bloom", QBloomRenderPass::Out::Result) + ) + .end("ToneMapping", QToneMappingRenderPass::Out::Result) //将色调映射的结果输出到屏幕上 +) +``` + +该代码搭建了如下的RenderGraph: + +![image-20231006140150745](Resources/image-20231006140150745.png) + +这个实现可以说是非常简单,这得益于QRhi已经完成了RenderGraph的一些职责。 + +[上一节](https://italink.github.io/ModernGraphicsEngineGuide/01-GraphicsAPI/6.%E5%9B%BE%E5%BD%A2%E6%B8%B2%E6%9F%93%E8%BF%9B%E9%98%B6/#_2) 中我们提到: **现代图形API要求手动管理资源的同步** 。 + +现代图形引擎则会在RenderGraph中,分析资源的依赖关系,来自动管理资源的同步。 + +而在QRhi中,已经实现了这部分逻辑,所以我们无需关心。 + +但上述的实现并不完美: + +- FrameGraph的构建位于主线程中,一次性创建过多的资源将造成UI的卡顿 + +- FrameGraph的结构固化,因为是一次创建整个FrameGraph,将其装配到渲染器中,现有的代码结构想要动态的调整渲染结构是一件很困难的事情,假如我能判断当前相机空间内不包含任何发光的物体,那么是否意味着可以把`BloomPass`给去掉?而去掉`BloomPass`其实就是将`BloomPass`的输入直接连接到之后的Pass上,就像是这样: + + image-20231006151018957 + +- 并没有很好的处理资源之间的依赖和重建,打个比方,假如窗口的尺寸发生变动,某些Pass中的RT就需要重建,而RT中的Texture重建,也就意味着之后使用该Texture的 **ShaderResourceBindings** 也需要重建,而当下的做法就很蠢:当窗口尺寸变动时,会重建整个FrameGraph中的所有资源。因为资源的创建全部放到了 **IRenderPassNode** 中的`onRecrecteResource`函数中,该函数包含了所有的资源创建,为了图省事就统一调用了它,虽然后面笔者增加了另一个抽象函数`onResizeAndLink`,在里面添加RT和Bindings的重建逻辑,但这部分结构依旧比较难用。 + +虽然这个FrameGraph很拉垮,但 "又不是不能用": + +img + +> 要不是为了出这篇文章,我肯定是不会去爆肝重构的T.T + +重构的接口方面主要参考了UE的RDG,上述代码在重构后变成了这样: + +``` c++ +class MyRenderer: public IRenderer { + Q_OBJECT + Q_PROPERTY_VAR(int, BlurIterations) = 2; + Q_PROPERTY_VAR(int, BlurSize) = 20; + Q_PROPERTY_VAR(int, DownSampleCount) = 4; + + Q_PROPERTY_VAR(float, Gamma) = 2.2f; + Q_PROPERTY_VAR(float, Exposure) = 1.f; + Q_PROPERTY_VAR(float, PureWhite) = 1.f; + + Q_CLASSINFO("BlurIterations", "Min=1,Max=8") +private: + QStaticMeshRenderComponent mStaticComp; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mStaticComp.setStaticMesh(QStaticMesh::CreateFromFile(RESOURCE_DIR"/Model/mandalorian_ship/scene.gltf")); + mStaticComp.setRotation(QVector3D(-90, 0, 0)); + + addComponent(&mStaticComp); + + getCamera()->setPosition(QVector3D(20, 15, 12)); + getCamera()->setRotation(QVector3D(-30, 145, 0)); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass"); + + QPixelFilterPassBuilder::Output filterOut = graphBuilder.addPassBuilder("FilterPass") + .setBaseColorTexture(meshOut.BaseColor) + .setFilterCode(R"( + const float threshold = 0.5f; + void main() { + vec4 color = texture(uTexture, vUV); + float value = max(max(color.r,color.g),color.b); + outFragColor = (1-step(value, threshold)) * color; + } + )"); + + QBlurPassBuilder::Output blurOut = graphBuilder.addPassBuilder("BlurPass") + .setBaseColorTexture(filterOut.FilterResult) + .setBlurIterations(BlurIterations) + .setBlurSize(BlurSize) + .setDownSampleCount(DownSampleCount); + + QBloomPassBuilder::Output bloomOut = graphBuilder.addPassBuilder("BloomPass") + .setBaseColorTexture(meshOut.BaseColor) + .setBlurTexture(blurOut.BlurResult); + + QToneMappingPassBuilder::Output tonemappingOut = graphBuilder.addPassBuilder("ToneMappingPass") + .setBaseColorTexture(bloomOut.BloomResult) + .setExposure(Exposure) + .setGamma(Gamma) + .setPureWhite(PureWhite); + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(tonemappingOut.ToneMappingReslut); + } +}; + +int main(int argc, char** argv) { + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} +``` + +其中比较大的变更如下: + +**RenderComponent** 存储在 **Renderer** 中,而不是像之前那样`MeshPass.addComponent` + +支持多线程,在RenderThread中驱动 **Renderer** **每帧** 去执行 `setupGraph` 函数,该函数主要职责是: + +- 利用 **QRenderGraphBuilder** 提供的一系列以`setup`开头的函数去创建资源描述,这些函数会 **尝试创建** RHI资源,并标记失效(跟之前参数不一致)的资源。 + + ``` c++ + void setupBuffer(QRhiBufferRef& buffer, const QByteArray& name, QRhiBuffer::Type type, QRhiBuffer::UsageFlags usages, int size); + + void setupTexture(QRhiTextureRef& texture, const QByteArray& name, QRhiTexture::Format format, const QSize& pixelSize, int sampleCount = 1, QRhiTexture::Flags flags = {}); + + void setupSampler(QRhiSamplerRef& sampler, + const QByteArray& name, + QRhiSampler::Filter magFilter, + QRhiSampler::Filter minFilter, + QRhiSampler::Filter mipmapMode, + QRhiSampler::AddressMode addressU, + QRhiSampler::AddressMode addressV, + QRhiSampler::AddressMode addressW = QRhiSampler::Repeat); + + void setupShaderResourceBindings(QRhiShaderResourceBindingsRef& bindings, const QByteArray& name, QVector binds); + + void setupRenderBuffer(QRhiRenderBufferRef& renderBuffer, + const QByteArray& name, + QRhiRenderBuffer::Type type, + const QSize& pixelSize, + int sampleCount = 1, + QRhiRenderBuffer::Flags flags = {}, + QRhiTexture::Format backingFormatHint = QRhiTexture::UnknownFormat); + + void setupRenderTarget(QRhiTextureRenderTargetRef& renderTarget, + const QByteArray& name, + const QRhiTextureRenderTargetDescription& desc, + QRhiTextureRenderTarget::Flags flags = {}); + + void setupGraphicsPipeline(QRhiGraphicsPipelineRef& pipeline, + const QByteArray& name, + const QRhiGraphicsPipelineState& state); + + void setupComputePipeline(QRhiComputePipelineRef& pipeline, + const QByteArray& name, + const QRhiComputePipelineState& state); + ``` + +- 通过`addPass`来添加真正的渲染逻辑 + + ``` c++ + void addPass(std::function executor) + ``` + +- 为了 **便于** Pass资源和功能的划分,提供了一个`IRenderPassBuilder`的抽象类,它的结构如下: + + ``` c++ + class IRenderPassBuilder { + friend class QRenderGraphBuilder; + virtual void setup(QRenderGraphBuilder& builder) = 0; + virtual void execute(QRhiCommandBuffer* cmdBuffer) = 0; + }; + ``` + + 在`setupGraph`中可以通过函数`QRenderGraphBuilder::addPassBuilder`来添加: + + ``` C++ + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass"); + } + ``` + + 它本质上等价于: + + ``` c++ + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + mMeshPassBuilder->setup(graphBuilder); + mMeshPassBuilder->addPass(std::bind(&QMeshPassBuilder::execute, mMeshPassBuilder, std::placeholders::_1)); + QMeshPassBuilder::Output meshOut = mMeshPassBuilder->getOutput(); + } + ``` + +我们可以按这样的结构来定义 **IRenderPassBuilder** + +``` c++ +class QPixelFilterPassBuilder : public IRenderPassBuilder { +public: + QPixelFilterPassBuilder(); + + QRP_INPUT_BEGIN(QPixelFilterPassBuilder) //定义输入 + QRP_INPUT_ATTR(QRhiTextureRef, BaseColorTexture); + QRP_INPUT_ATTR(QString, FilterCode); + QRP_INPUT_END() + + QRP_OUTPUT_BEGIN(QPixelFilterPassBuilder) //定义输出 + QRP_OUTPUT_ATTR(QRhiTextureRef, FilterResult); + QRP_OUTPUT_END() +public: + void setup(QRenderGraphBuilder& builder) override //装配各种资源描述,读取输入,填充输出 + { + if (mFilterCode != mInput._FilterCode) { //获取输入 + mFilterCode = mInput._FilterCode; + mFilterFS = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 450 + layout (binding = 0) uniform sampler2D uTexture; + layout (location = 0) in vec2 vUV; + layout (location = 0) out vec4 outFragColor; )" + + mFilterCode.toLocal8Bit()); + } + if (!mFilterFS.isValid()) + return; + + + builder.setupTexture(mRT.colorAttachment, "FilterTexture", mInput._BaseColorTexture->format(), mInput._BaseColorTexture->pixelSize(), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource); + builder.setupRenderTarget(mRT.renderTarget, "FilterRenderTarget", { mRT.colorAttachment.get() }); + builder.setupSampler(mSampler, "FilterSampler", + QRhiSampler::Linear, + QRhiSampler::Linear, + QRhiSampler::None, + QRhiSampler::ClampToEdge, + QRhiSampler::ClampToEdge); + + builder.setupShaderResourceBindings(mBindings, "FilterBindings", { + QRhiShaderResourceBinding::sampledTexture(0,QRhiShaderResourceBinding::FragmentStage,mInput._BaseColorTexture.get(), mSampler.get()), + }); + + QRhiGraphicsPipelineState PSO; + PSO.sampleCount = mRT.renderTarget->sampleCount(); + PSO.shaderResourceBindings = mBindings.get(); + PSO.renderPassDesc = mRT.renderTarget->renderPassDescriptor(); + PSO.shaderStages = { + { QRhiShaderStage::Vertex, builder.getFullScreenVS() }, + { QRhiShaderStage::Fragment, mFilterFS } + }; + builder.setupGraphicsPipeline(mPipeline, "FilterPipeline", PSO); + mOutput.FilterResult = mRT.colorAttachment; //填充输出 + } + + void execute(QRhiCommandBuffer* cmdBuffer) override //执行渲染逻辑 + { + if (!mFilterFS.isValid()) + return; + cmdBuffer->beginPass(mRT.renderTarget.get(), QColor::fromRgbF(0.0f, 0.0f, 0.0f, 0.0f), { 1.0f, 0 }); + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mRT.renderTarget->pixelSize().width(), mRT.renderTarget->pixelSize().height())); + cmdBuffer->setShaderResources(mBindings.get()); + cmdBuffer->draw(4); + cmdBuffer->endPass(); + } + +private: + QShader mFilterFS; + QString mFilterCode; + struct RTResource { + QRhiTextureRef colorAttachment; + QRhiTextureRenderTargetRef renderTarget; + }; + RTResource mRT; + QRhiGraphicsPipelineRef mPipeline; + QRhiSamplerRef mSampler; + QRhiShaderResourceBindingsRef mBindings; +}; +``` + +### 设计原则 + +综上,RenderGraph的主要设计原则有: + +- 为了能够让渲染的结构是 **动态可变的** ,会有一个`Setup`阶段, **每帧** 都在重建RenderGraph(只是 **创建资源描述** 和 **组织依赖关系** ,并添加 **Executor** 逻辑) +- 在`Setup`完毕之后,进行`Compile`过程,此时我们可以拿到 **当前帧** 所有的资源描述和依赖关系,这个时候才会根据 资源描述 创建(或从池中复用)真正的 **RHI资源** ,并根据依赖资源间的依赖关系,在 **Executors 之间 穿插 资源同步的Execute指令** ,并处理一些其他的执行优化。 +- 在`Compile`阶段之后,进入到`Execute`阶段,此时所有的 **资源描述句柄** 都对应了实际的RHI资源,所有的RHI相关的逻辑都应该在 **Executor** 中。 +- `Execute`阶段执行结束,会 **清理资源描述和RHI资源的绑定** ,RHI资源会回归到池中,因为RenderGraph的每次执行之间不存在持久性,这意味着在RenderGraph执行一遍`Setup`创建的 **资源描述 不需要延续到下一帧** (一些特例除外)。 + +用QRhi去实现RenderGraph的结构并不困难,因为它本身就已经做了资源的同步,而使用`QRhi::new...()`得到的就是 资源描述,只有当调用`create`函数时才真正创建RHI资源,由于QRhi的实现有些特殊,因此笔者并没有完全遵照上述的设计原则去管理资源,关键还是照顾当下的使用需求。 + +## MeshPass + +MeshPass是一个非常特殊的RenderPass,因为三维空间的 几何图形 几乎都在这个Pass中进行绘制,而其他RenderPass的重点往往只是对图像进行处理,而不是几何图元的绘制。 + +在上文的设计思想下,我们的MeshPass的`execute`函数大概是这个样子: + +``` c++ +void execute(QRhiCommandBuffer* cmdBuffer){ + QRhiResourceUpdateBatch* batch = rhi->nextResourceUpdateBatch(); + + for(auto item: renderItems){ + item->tryCreate(rhi,mRenderTarget); + item->tryUpload(batch); + item->updated(batch); + } + + cmdBuffer->beginPass(mRenderTarget, clearColor, dsClearValue, batch); + + for(auto item: renderItems) + item->draw(cmdBuffer,viewport); + + cmdBuffer->endPass(); +} +``` + +而 **IRenderItem** 的抽象结构大概是这样的: + +``` c++ +class IRenderItem{ +protected: + virtual void create(QRhi* rhi, QRhiRenderTaget* rt) = 0; + virtual void upload(QRhiResourceUpdateBatch* batch) = 0; +public: + void tryCreate(QRhi* rhi,QRhiRenderTaget* rt){ + if(needCreate()) + create(rhi,rt); + } + void tryUpload(QRhiResourceUpdateBatch* batch{ + if(needUpload()) + upload(batch); + } + virtual void updated(QRhiResourceUpdateBatch* batch) = 0; + virtual void draw(QRhi* commandBuffer, QRhiViewport viewport) = 0; +} +``` + +如果我们想实现一个能绘制三角形的RenderItem,只需实现这样的结构: + +``` c++ +class QTriangleRenderItem: public IRenderItem{ +public: + QSharedPointer mVertexBuffer; + QSharedPointer mShaderBindings; + QScopedPointer mPipeline; +protected: + void create(QRhi* rhi, QRhiRenderTaget* rt) override{ + mVertexBuffer = ...; + mShaderBindings = ...; + mPipeline = ...; + } + void upload(QRhiResourceUpdateBatch* batch) override{ + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + } + void updated(QRhiResourceUpdateBatch* batch) override{} + void draw(QRhi* cmdBuffer, QRhiViewport viewport) override{ + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(viewport); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexInput(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexInput); + cmdBuffer->draw(3); + } +} +``` + +这样的结构扩展起来非常容易,但随着一些功能需求的出现,问题才慢慢暴露出来: + +- 有可能存在多种类型的MeshPass,比如正常的MeshPass的RT带有一个颜色附件,但延迟渲染的MeshPass中可能带有多个附件,比如PBR的就有`BaseColor`,`Position`,`Normal`,`Metallic`,`Roughness`,MeshPass的RT将会影响到 RenderItem 中流水线的 **SampleCount** , **RenderPassDescriptor** , **TargetBlends** ,以及我们要在 **FragmentShader** 中如何填充对应的颜色附件。 +- 我们可能希望在全局复用Render Item的流水线数据,生成新的流水线,并顶替掉其中一些状态,来完成某些功能,比如: + - 制作鼠标点选,我们可以在一张RT上,绘制所有Item的ID,当鼠标点击时,将点击的像素回读到CPU,一比对就能知道我们点击的是哪个Item,这需要我们覆盖流水线的 **FragmentShader** 和 **StencilState** + - 制作ShadowMap,需要从相机视角绘制场景的深度,这需要我们覆盖流水线的 **ShaderResourceBindings(ViewMatrix)** 和 **FragmentShader** + +所以在设计 RenderItem 的时候,如果直接操作 RHI 的资源 和 指令,将会使我们后续的一些功能扩展受限。 + +为此我们得进一步封装,让流水线的使用具备一定弹性。 + +这里笔者封装的结构是 **QPrimitiveRenderProxy** ,它提供了以下功能: + +- 包裹 **QRhiGraphicsPipeline** 的创建,提供 MeshPass Render Target 适配功能。 +- 封装 **QRhiUniformBlock** ,可以以简单的方式快速为流水线创建Uniform参数。 +- 封装纹理资源,可以自动添加绑定,并生成Shader定义。 +- 将 **Create** , **Upload** , **Update** , **Draw** 的逻辑作为回调函数存储起来,本质上它也是一种可被复用的状态。 +- 提供SubPipeline的机制,可以以Proxy为模板,创建子管线,顶替掉一些流水线状态, 当Proxy发生变动时,SubPipeline也会重建。 + +在此基础上,如果想完成一个三角形的绘制,只需编写这样的代码: + +``` c++ +class QTriangleRenderComponent: public IRenderComponent { // IRenderComponent对应文章中的IRenderItem + Q_OBJECT + Q_PROPERTY(QColor Color READ getColor WRITE setColor) +public: + QColor getColor() const { return mColor; } + void setColor(QColor val) { mColor = val; } +private: + QScopedPointer mVertexBuffer; + QSharedPointer mProxy; + QColor mColor = QColor::fromRgbF(0.1f, 0.5f, 0.9f, 1.0f); +protected: + void onRebuildResource() override { + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + mVertexBuffer->create(); + + mProxy = newPrimitiveRenderProxy(); + + mProxy->addUniformBlock(QRhiShaderStage::Fragment, "UBO") + ->addParam("Color", mColor); + + mProxy->setInputBindings({ + QRhiVertexInputBindingEx(mVertexBuffer.get(), 2 * sizeof(float)) + }); + + mProxy->setInputAttribute({ + QRhiVertexInputAttributeEx("inPosition",0, 0, QRhiVertexInputAttribute::Float2, 0), + }); + mProxy->setShaderMainCode(QRhiShaderStage::Vertex, R"( + void main(){ + gl_Position = vec4(inPosition, 0.0f,1.0f); + } + )"); + mProxy->setShaderMainCode(QRhiShaderStage::Fragment, QString(R"( + void main(){ + %1 + })") + .arg(hasColorAttachment("BaseColor")? "BaseColor = UBO.Color;" : "") + .toLocal8Bit() + ); + mProxy->setOnUpload([this](QRhiResourceUpdateBatch* batch) { + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + }); + mProxy->setOnUpdate([this](QRhiResourceUpdateBatch* batch, const QPrimitiveRenderProxy::UniformBlocks& blocks, const QPrimitiveRenderProxy::UpdateContext& ctx) { + blocks["UBO"]->setParamValue("Color", QVariant::fromValue(mColor)); + }); + mProxy->setOnDraw([this](QRhiCommandBuffer* cmdBuffer) { + const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexBindings); + cmdBuffer->draw(3); + }); + } +}; +``` + +### Material + +关于材质,笔者首次接触这个概念是在 [Learn OpenGL](https://learnopengl-cn.github.io/02%20Lighting/03%20Materials/) ,在当时的理解中,材质就是一个这样的UniformBlock: + +``` c++ +struct Material { + vec3 ambient; + vec3 diffuse; + vec3 specular; + float shininess; +}; +``` + +但在接触过一些引擎之后,才发现材质并没有那么简单: + +- [Unreal Engine | Material](https://docs.unrealengine.com/5.3/en-US/unreal-engine-materials/) + +- [O3DE | Material](https://docs.o3de.org/docs/atom-guide/look-dev/materials/material-system/) + +- [Unity | Material](https://docs.unity3d.com/Manual/materials-introduction.html) + +- [Godot | Material](https://docs.godotengine.org/en/stable/classes/class_material.html) + +在各个引擎中材质系统实现都有所差异,但它们都遵循这一原则:材质定义了图形的表面属性,它可以被视为控制物体视觉外观的"皮肤"。 + +Unreal Engine 中的材质系统无疑是最为强大的,它的材质系统涵盖了所有控制图形外观功能,它提供一系列的参数去控制流水线 裁剪测试,深度测试,模板测试,混合模式,背面剔除... + +针对不同的 **着色模型(Shading Model)** 提供 材质表达式插槽,它提供了大量的材质函数节点,可以将其转换为对应的Shader代码,还能轻松创建着色器资源(Buffer,Sampler和Texture),并自动创建着色器资源绑定。 + +![image-20231006192226000](Resources/image-20231006192226000.png) + +材质系统的架构需要考虑引擎的整个渲染体系,由于本教程更多的是进行渲染尝试(主要是因为没有精力),因此在笔者封装的 **QMaterial** 中,它只是一个简单提供参数和纹理的结构: + +``` C++ +struct QMaterial { + QMap mProperties; +}; +``` + +在 **QPrimitiveRenderProxy** 中,添加材质会自动为这些属性创建 **UniformBuffer** 和 **Texture** ,供 MeshPass 中构建 **RenderComponet** 时使用 + +关于具体的使用,请参考: + +- https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/2-EngineTechnology/00-RenderingArchitecture/Source/main.cpp + +![image-20231006201742745](Resources/image-20231006201742745.png) + +``` c++ +#include "QEngineApplication.h" +#include "QRenderWidget.h" +#include "Render/IRenderComponent.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" + +static float VertexData[] = { + //position(xy) + 0.0f, -0.5f, + -0.5f, 0.5f, + 0.5f, 0.5f, +}; + +class QTriangleRenderComponent: public IRenderComponent { + Q_OBJECT + Q_PROPERTY(QColor Color READ getColor WRITE setColor) +public: + QColor getColor() const { return mColor; } + void setColor(QColor val) { mColor = val; } +private: + QScopedPointer mVertexBuffer; + QSharedPointer mProxy; + QColor mColor = QColor::fromRgbF(0.1f, 0.5f, 0.9f, 1.0f); +protected: + void onRebuildResource() override { + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + mVertexBuffer->create(); + + mProxy = newPrimitiveRenderProxy(); + + mProxy->addUniformBlock(QRhiShaderStage::Fragment, "UBO") + ->addParam("Color", mColor); + + mProxy->setInputBindings({ + QRhiVertexInputBindingEx(mVertexBuffer.get(),2 * sizeof(float)) + }); + + mProxy->setInputAttribute({ + QRhiVertexInputAttributeEx("inPosition",0, 0, QRhiVertexInputAttribute::Float2, 0), + }); + mProxy->setShaderMainCode(QRhiShaderStage::Vertex, R"( + void main(){ + gl_Position =vec4(inPosition, 0.0f,1.0f); + } + )"); + mProxy->setShaderMainCode(QRhiShaderStage::Fragment, QString(R"( + void main(){ + %1 + })") + .arg(hasColorAttachment("BaseColor")? "BaseColor = UBO.Color;" : "") + .toLocal8Bit() + ); + mProxy->setOnUpload([this](QRhiResourceUpdateBatch* batch) { + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + }); + mProxy->setOnUpdate([this](QRhiResourceUpdateBatch* batch, const QPrimitiveRenderProxy::UniformBlocks& blocks, const QPrimitiveRenderProxy::UpdateContext& ctx) { + blocks["UBO"]->setParamValue("Color", QVariant::fromValue(mColor)); + }); + mProxy->setOnDraw([this](QRhiCommandBuffer* cmdBuffer) { + const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexBindings); + cmdBuffer->draw(3); + }); + } +}; + +class QOutliningPassBuilder : public IRenderPassBuilder { + QRP_INPUT_BEGIN(QOutliningPassBuilder) + QRP_INPUT_ATTR(QRhiTextureRef, BaseColor); + QRP_INPUT_END() + + QRP_OUTPUT_BEGIN(QOutliningPassBuilder) + QRP_OUTPUT_ATTR(QRhiTextureRef, OutliningResult) + QRP_OUTPUT_END() +private: + QRhiTextureRef mColorAttachment; + QRhiTextureRenderTargetRef mRenderTarget; + QShader mOutliningFS; + QRhiSamplerRef mSampler; + QRhiShaderResourceBindingsRef mOutliningBindings; + QRhiGraphicsPipelineRef mOutliningPipeline; +public: + QOutliningPassBuilder() { + mOutliningFS = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 450 + layout (location = 0) in vec2 vUV; + layout (location = 0) out vec4 outFragColor; + + layout (binding = 0) uniform sampler2D uBaseColor; + + void main() { + vec2 texOffset = 1.0 / textureSize(uBaseColor, 0); // gets size of single texel + + vec3 maxdiff = vec3(0.f); + vec3 center = texture(uBaseColor,vUV).rgb; + + maxdiff = max(maxdiff, abs(texture(uBaseColor,vUV+vec2(texOffset.x,0)).rgb - center)); + maxdiff = max(maxdiff, abs(texture(uBaseColor,vUV-vec2(texOffset.x,0)).rgb - center)); + maxdiff = max(maxdiff, abs(texture(uBaseColor,vUV+vec2(0,texOffset.y)).rgb - center)); + maxdiff = max(maxdiff, abs(texture(uBaseColor,vUV-vec2(0,texOffset.y)).rgb - center)); + + const vec4 outliningColor = vec4(1.0,0.0,0.0,1.0); + + outFragColor = length(maxdiff) > 0.1 ? outliningColor : vec4(center,1.0f); + + } + )"); + } + void setup(QRenderGraphBuilder& builder) override { + builder.setupTexture(mColorAttachment, "Outlining", QRhiTexture::Format::RGBA32F, mInput._BaseColor->pixelSize(), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource); + builder.setupRenderTarget(mRenderTarget, "OutliningRT", QRhiTextureRenderTargetDescription(mColorAttachment.get())); + + builder.setupSampler(mSampler, "OutliningSampler", QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge); + + builder.setupShaderResourceBindings(mOutliningBindings, "OutliningBindings", { + QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage,mInput._BaseColor.get() ,mSampler.get()), + }); + QRhiGraphicsPipelineState PSO; + PSO.shaderResourceBindings = mOutliningBindings.get(); + PSO.sampleCount = mRenderTarget->sampleCount(); + PSO.renderPassDesc = mRenderTarget->renderPassDescriptor(); + QRhiGraphicsPipeline::TargetBlend targetBlends; + targetBlends.enable = true; + PSO.targetBlends = { targetBlends }; + PSO.shaderStages = { + QRhiShaderStage(QRhiShaderStage::Vertex, builder.getFullScreenVS()), + QRhiShaderStage(QRhiShaderStage::Fragment, mOutliningFS) + }; + builder.setupGraphicsPipeline(mOutliningPipeline, "OutliningPipeline", PSO); + + mOutput.OutliningResult = mColorAttachment; + } + void execute(QRhiCommandBuffer* cmdBuffer) override { + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + cmdBuffer->beginPass(mRenderTarget.get(), clearColor, dsClearValue); + cmdBuffer->setGraphicsPipeline(mOutliningPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mRenderTarget->pixelSize().width(), mRenderTarget->pixelSize().height())); + cmdBuffer->setShaderResources(mOutliningBindings.get()); + cmdBuffer->draw(4); + cmdBuffer->endPass(); + } +}; + +class MyRenderer : public IRenderer { +private: + QTriangleRenderComponent mComp; +public: + MyRenderer() + : IRenderer({QRhi::Vulkan}) + { + addComponent(&mComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshPassOut = graphBuilder.addPassBuilder("MeshPass"); + + QOutliningPassBuilder::Output outliningOut = graphBuilder.addPassBuilder("OutliningPass") + .setBaseColor(meshPassOut.BaseColor); + + QOutputPassBuilder::Output ret = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(outliningOut.OutliningResult); + } +}; + +int main(int argc, char **argv){ + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} + +#include "main.moc" +``` + diff --git "a/Docs/02-EngineTechnology/1.\347\274\226\350\276\221\345\231\250\346\236\266\346\236\204.md" "b/Docs/02-EngineTechnology/1.\347\274\226\350\276\221\345\231\250\346\236\266\346\236\204.md" new file mode 100644 index 00000000..417773aa --- /dev/null +++ "b/Docs/02-EngineTechnology/1.\347\274\226\350\276\221\345\231\250\346\236\266\346\236\204.md" @@ -0,0 +1,343 @@ +--- +comments: true +--- + +# 编辑器架构 + +笔者近几年开发中的绝大部分代码积累,大多花在了引擎编辑器上,在此处罗列一些心得,给大家一些参考,文章会从以下几个维度去分析怎么去做好编辑器的设计: + +- 设计美感 +- 用户体验 +- 基于反射的编辑器架构 +- 性能优化 + +## 设计美感 + +设计美感是大多数开发者所欠缺的东西,因为他们在代码上花费了非常多的心思,所以很少人会愿意绞尽脑汁去思考怎样去制作出精美的界面。 + +当然,虽然他们没有一个懂得设计的大脑,但却也长了一双欣赏美丽事物的眼睛,只要有合适的参考,开发者们能很快的复刻出相应的效果。 + +在大多数软件开发团队中,往往会有专业的UI设计师,他们主要关注UI的: + +- 排版 +- 色调 +- 图标 +- 布局 + +在笔者使用过的一些软件系统中,有一部分虽然确实考虑了以上几点,制作出了精美的界面效果,但往往忽略了UI的 **组件化设计** ,从而导致: + +- 用户无法快速建立起软件的使用规律,产生了很大的认知负担。 +- 破坏软件代码的组织架构,一个组件化设计优秀的UI,可以从界面上看出代码的层次结构,一个条例混乱的界面,它的代码往往也是乱作一团。 + +以UnrealEngine为例,它的组件化设计就很直观: + +![image-20230629110339347](Resources/image-20230629110339347.png) + +用户只需理解和掌握界面上各个组件元素的职责,很多时候都可以在不借助教程或文档的情况下,举一反三地进行操作。 + +UI设计师大多专注于于UI效果,因此让他们去制作一个适用于代码架构的组件化设计往往是比较困难的,所以关于这方面的内容,需要开发者去思考,讨论,引导,并学会说 "不"。 + +关于UI设计上的一些概念和风格,这里有更详细的参考: + +- [All Time Design - UI Style Guide](https://alltimedesign.com/ui-style-guide/) + +## 用户体验 + +关于用户体验,主要围绕: + +- 减少用户的认知负担:在界面布局,标签命名,操作上遵循一些常识和规范 +- 支持撤销,重做:让一些误操作能够被快速修正 +- 合理的操作反馈:当执行一些操作时,可以使用日志,对话框,进度条,通知气泡,状态栏...来告知用户 操作的执行状态 +- 及时的状态备份或保存:避免意料之外的崩溃导致大量工作数据的丢失 +- 可配置的操作方式:让用户可以自定义快捷键,布局方式,操作风格等 + +关于减少用户的认知负担,这里有一个非常完善的文档: + +- [O3DE - Component Card UX Pattern](https://docs.o3de.org/docs/tools-ui/ux-patterns/component-card/) + +- [O3DE - Component Card Error Handling](https://docs.o3de.org/docs/tools-ui/ux-patterns/error/) + +关于撤销重做,它是一个代码设计的问题,通常情况下,我们会在设计模式层面使用备忘录模式和命令模式来实现撤销重做功能,这在Qt中对应的结构是 **QUndoCommand** 和 **QUndoStack** ,其中 **QUndoCommand** 的核心定义如下: + +``` c++ +class QUndoCommand +{ + virtual void undo(); + virtual void redo(); + virtual int id() const; + virtual bool mergeWith(const QUndoCommand *other); +}; +``` + +在使用时,我们会派生QUndoCommand,覆写函数去实现undo和redo的操作行为,之后再将具体的Command推入到UndoStack中。 + +这里有一个简单的代码示例: + +``` c++ +#include +#include +#include + +class QAssignCommand : public QUndoCommand { +public: + QAssignCommand(int* ptr, int dstValue) + : mValuePtr(ptr) + , mSrcValue(*ptr) + , mDstValue(dstValue) {} +protected: + virtual void undo() override { + *mValuePtr = mSrcValue; + } + virtual void redo() override { + *mValuePtr = mDstValue; + } +private: + int* mValuePtr = nullptr; + int mSrcValue; + int mDstValue; +}; + +int main(int argc, char** argv) { + QUndoStack stack; + int value = 0; + stack.push(new QAssignCommand(&value, 1)); + qDebug() << value; //1 + + stack.undo(); + qDebug() << value; //0 + + stack.redo(); + qDebug() << value; //1 + + stack.beginMacro("Merge"); + stack.push(new QAssignCommand(&value, 2)); + stack.push(new QAssignCommand(&value, 3)); + stack.endMacro(); + qDebug() << value; //3 + + stack.undo(); + qDebug() << value; //1 + + return 0; +} +``` + +它的实现思路比较简单,但也有一些比较难处理的点: + +- Command中操作对象如果在其他地方被销毁了,但Command还存在于Stack中,如果此时执行撤销重做,可能会导致程序的崩溃 + +对于这种问题,我们主要可以通过以下几个方案去解决: + +- 考虑是不是撤销重做操作的指令没有完全覆盖操作对象的生命周期 + +- 为Command增加有效性验证 +- 使用序列化 对 对象 进行保存和重建 + +在Unreal Engine,提供了一种基于对象系统的撤销重做机制 —— **Transactional** + +对于任意携有`RF_Transactional`标识的UObject实例,都可以使用如下代码来实现撤销重做 + +``` c++ +GEditor->BeginTransaction(FText::FromString("Assign Value")); //开始事务 + +MyObject->Modify(); //保存初始状态 +MyObject->ValueProperty = 123; //修改属性 + +GEditor->EndTransaction(); //结束事务 +``` + +由于UObject的GC可以让对象延迟回收,这也使得我们无需考虑操作对象生命周期的问题,关于它的实现原理,详见: + +- [CSDN - UE4 编辑器(UObject)的Undo撤回系统Transactional](https://blog.csdn.net/qq_29523119/article/details/96778797) + +关于状态保存,相信大家都遇到过程序突然崩溃或者电脑突然关机的情况,这个时候如果没有及时的保存工作数据,几小时的工作成果就得打水漂了,而这个时候,如果程序提供保存或备份的辅助手段,在意外来临时,就能救自己一命~ + +而这种手段一般是指自动保存,除了让用户手动点击去保存工作数据,我们还可以在特定时机去自动保存或备份工作数据,就比如: + +- 用户关闭某个界面时进行保存 +- 定时自动保存 +- 程序崩溃时保存(使用`SetUnhandledExceptionFilter`函数可设置程序崩溃时的系统回调) + +上述手段可以尽可能的减少工作数据丢失的风险,当然,对于一些保存性能损耗不大的数据,比如小型配置文件,就可以直接在执行完操作后,立即保存。 + +## Meta思维 + +Meta思维在现代工业软件开发过程中可以提供极大的便捷度,它能提供一劳永逸的代码扩展方式。 + +> 关于 meta 思维的联想词有:`求导`,`高阶维度`,`上帝视角`,... +> +> 详见: [[ 知乎 ] bus waiter- 谈meta](https://zhuanlan.zhihu.com/p/406257437) + +假如将写代码比作盖房子,我们考虑的是如何盖好房子,那在Meta层面,我们考虑的是如何构建一台自动盖房子的机器,Meta思维让我们从一个工程问题转换成了另一个,虽然难度有所上升,但只要方法得当,它能带来无可比拟的收益。 + +笔者接触过一些自研引擎和软件,只要涉及到 **自研引擎编辑器** 相关的话题,那下面基本就是骂声一片~~~ + +究其原因,无外乎: + +- 界面简陋 +- 用户体验差 + +而大部分开发者的想法是: + +- “并不是不能做好,只是要做好得花很多时间,会很麻烦,没必要浪费时间,将就能用就行了。” + +当然,这种想法对一些小工具来说无可厚非,但是对于一些高频使用的工具,如果依旧是这样的态度对待,那对使用人员来说,就是一场痛苦的折磨... + +笔者在Qt上有数十万行代码的积累,但在Unreal Engine上只使用了数万行的代码,却出做出了更多更完备的功能,并且在Unreal Engine中,界面风格和用户体验的需求是非常容易满足的,这很大程度得益于 Unreal Engine 的 “ **基础建设** ” 。 + +在游戏引擎中,基于Meta理念的架构比比皆是,而在这其中最有用最强大的无疑是 —— **基于反射的编辑器架构** + +相信大家都见过一些参数调节面板,比如: + +![image-20230628222459585](Resources/image-20230628222459585.png) + +在代码层面,这些可配置参数往往对应具体的数据结构,它可能是这样的: + +``` c++ +struct Options{ + bool Option0; + int Option1; + string Option2; + vector Option3; +}; +``` + +大部分开发者为针对数据结构`Options`去手动编写代码去创建编辑面板,而在Meta层面,我们会对这类复杂结构进一步拆解,因为C++中的基本类型是有限的,开发者所创建的类型也只是通过那些基础类型按照一定层次组织起来的,我们完全可以根据成员变量的类型来创建控件,根据`Options`的结构来组织控件的布局,简而言之,也就是 **基于类型的控件,基于结构的面板** 。 + +由于我们需要在运行时获取到数据结构的相关信息,因此就必须依靠 **反射** ,此外,数据结构组织往往是树状的,因此我们会使用GUI框架中的`TreeView控件`来组织控件面板。 + +这里有一个简单的伪代码: + +``` c++ +class Options: public Object{ //数据结构,Object是对象系统的基类 + OBJECT_ENTRY() //OBJECT_ENTRY是反射系统的入口宏 + + PROPERTY() bool Option0; //使用PROPERTY宏反射属性 + PROPERTY() int Option1; + PROPERTY() string Option2; + PROPERTY() vector Option3; +}; + +class DetailView: public TreeView{ +public: + void setObject(Object* object){ + clearTreeView(); + for(auto metaProperty: object->metaObject()){ //根据MetaObject遍历对象所有的属性 + Widget* widget = generateWidgetForType(metaProperty.type()); //根据类型创建控件 + widget->bind(Object,metaProperty); //将控件于反射的属性进行绑定 + this->addRow(widget); //将控件添加到TreeView中 + } + } +}; +``` + +对于类型,我们主要根据以下分类去归纳: + +- 基本类型:逻辑,数值,枚举,字符串 +- 复合类型:类,结构体 +- 容器类型: + - 连续(sequential)容器:list,array,vector,set,queue... + - 组合(associative)容器:map,hashmap... + +通过这样的分类,开发者只需要为基本类型制作控件,为复合类型组织属性面板布局,而容器类型的编辑面板,往往是通过内部元素类型自动生成和组合的,此外,为了能够让开发者能够随心所欲的扩展基本属性和覆盖对象的面板和布局,一般会提供两个结构: + +```C++ +class IDetailCustomization{ +public: + virtual void CustomizeDetails(DetailsBuilder* builder) = 0; //用于自定义对象的调节面板 +}; + +class IPropertyTypeCustomization{ +public: + virtual void CustomizeHeader(RowBuilder* builder) = 0; //用于自定义属性类型的 Header Row + virtual void CustomizeChildren(DetailsBuilder* builder) {} //用于附加该属性类型的 Child Row +}; +``` + +为了能够管理属性,还会派生`IPropertyHandle`,它主要用作: + +- 使用多态处理层次关系的组织。 +- 作为属性设置和获取的唯一入口,加入撤销重做,变动通知,编辑器同步等相关功能。 + +``` c++ +class IPropertyHandle{ + void SetValue(Variant var); + Variant GetValue(); + virtual Widget* GenerateNameWidget(); //生成属性的名称控件 + virtual Widget* GenerateValueWidget(); //生成属性的值控件 + virtual void GenerateChildrenRow(DetailsBuilder* builder); //生成属性的子行 +}; +``` + +有了如上结构的支撑,DetailsView的创建过程可以简单看作是: + +``` C++ +map GloabalDetailCustomizationMap; +map GloabalPropertyTypeCustomizationMap; + +class DetailView: public TreeView{ +public: + void setObject(Object* object){ + clearTreeView(); + + auto DetailCustomization = GloabalDetailCustomizationMap[object->metaObject()->type()]; + DetailsBuilder builder(this); + DetailCustomization->CustomizeDetails(&builder); + /* 获取到对象类型所对应的DetailCustomization,调用CustomizeDetails会遍历其所有的Property, + 并创建IPropertyHandle,而IPropertyHandle又会根据属性类型所对应的IPropertyTypeCustomization, + 创建具体的属性调节控件 + */ + } +}; +``` + +实际的架构会让比上面的结构复杂一些,但只要掌握了核心思路,实现起来并不困难,此处有一个简单版本的Qt实现(参考了Unreal Engine 的 DetailsView): + +- 代码:https://github.com/Italink/QEngineUtilities/tree/main/Source/Editor/Source/Public/DetailView +- 示例工程:https://github.com/Italink/QEngineUtilities/tree/main/Samples/DetailViewExample + +![image-20230628234033004](Resources/image-20230628234033004.png) + +## 性能优化 + +计算机的计算资源始终是有限的,而在游戏引擎中,性能消耗的大头毫无疑问是Game,因此我们需要尽可能的降低编辑器的性能损耗以减少它对Game执行的影响。 + +一般情况下,我们可以通过以下手段来优化编辑器的性能(游戏内UI也适用): + +- **不要为了追求美观和使用高损耗的界面效果** : + + - 尽可能不去使用界面动画和滤镜(模糊,阴影,拟态,毛玻璃,投影变换...) + + - 不要使用高分辨率的图像资源 + + - 对于静态的UI,尽可能使用已制作好的图像,或者预先绘制RT(Bitmap),而不是使用GUI框架每帧实时绘制,如取色器的调色板: + + ![image-20230629115539526](Resources/image-20230629115539526.png) + +- **管控界面重绘的时机** :假如某个`操作A`会触发界面更新,但有时候我们往往在一帧执行多次`操作A`,很显然我们没必须要一帧重绘多次界面,所以我们往往不会在`操作A`中直接更新UI,而是设置标识,在UI的`Tick`事件中,根据标识去重绘UI,就像是这样(伪代码): + + ``` c++ + class MyWidget : public Widget{ + private: + bool bNeedRepaint = false; + public: + void setFont(...){ //设置字体,触发界面重绘 + bNeedRepaint = true; + } + + void tick() override{ //覆写UI的Tick事件,在Tick时检查是否需要重绘UI + if(bNeedRepaint){ + repaintUI(); //UI的重绘接口 + bNeedRepaint = false; + } + } + }; + ``` + +- **使用离屏绘制缓存和局部刷新** :对于一些上帧图像复用性很高的UI,可以将UI先绘制到一张RT(Bitmap)上,每帧只要按需更新部分区域的UI即可,这种做法在图表,画布相关的UI上较为常见。 + +- **避免主线程中执行高消耗的逻辑操作阻塞UI** :UI的处理往往是位于主线程,如果一些在主线程逻辑操作的执行耗时比较高,就会导致UI的阻塞,解决这个问题主要有两个方法: + + - 在逻辑操作中插入UI系统的更新操作强制UI刷新。 + - 将逻辑操作放到异步线程去执行,逻辑执行完毕,再在主线程中执行UI相关的更新。 + diff --git a/Docs/02-EngineTechnology/Resources/faf2b2119313b07e4e15ba5539b35f2896dd8cb7.jpeg b/Docs/02-EngineTechnology/Resources/faf2b2119313b07e4e15ba5539b35f2896dd8cb7.jpeg new file mode 100644 index 00000000..afd96e6e Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/faf2b2119313b07e4e15ba5539b35f2896dd8cb7.jpeg differ diff --git a/Docs/02-EngineTechnology/Resources/image-20230628222459585.png b/Docs/02-EngineTechnology/Resources/image-20230628222459585.png new file mode 100644 index 00000000..b8f8f81f Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20230628222459585.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20230628234033004.png b/Docs/02-EngineTechnology/Resources/image-20230628234033004.png new file mode 100644 index 00000000..b3114638 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20230628234033004.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20230629110339347.png b/Docs/02-EngineTechnology/Resources/image-20230629110339347.png new file mode 100644 index 00000000..2b2ee12b Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20230629110339347.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20230629110341537.png b/Docs/02-EngineTechnology/Resources/image-20230629110341537.png new file mode 100644 index 00000000..2b2ee12b Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20230629110341537.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20230629115539526.png b/Docs/02-EngineTechnology/Resources/image-20230629115539526.png new file mode 100644 index 00000000..8d70b39e Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20230629115539526.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006114116772.png b/Docs/02-EngineTechnology/Resources/image-20231006114116772.png new file mode 100644 index 00000000..eb651746 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006114116772.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006120654050.png b/Docs/02-EngineTechnology/Resources/image-20231006120654050.png new file mode 100644 index 00000000..907e6cd9 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006120654050.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006120808600.png b/Docs/02-EngineTechnology/Resources/image-20231006120808600.png new file mode 100644 index 00000000..907e6cd9 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006120808600.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006123724423.png b/Docs/02-EngineTechnology/Resources/image-20231006123724423.png new file mode 100644 index 00000000..e963377c Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006123724423.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006124210852.png b/Docs/02-EngineTechnology/Resources/image-20231006124210852.png new file mode 100644 index 00000000..6c054704 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006124210852.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006125745195.png b/Docs/02-EngineTechnology/Resources/image-20231006125745195.png new file mode 100644 index 00000000..fb63dd47 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006125745195.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006130046879.png b/Docs/02-EngineTechnology/Resources/image-20231006130046879.png new file mode 100644 index 00000000..0567bab2 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006130046879.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006131116388.png b/Docs/02-EngineTechnology/Resources/image-20231006131116388.png new file mode 100644 index 00000000..51512f53 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006131116388.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006140150745.png b/Docs/02-EngineTechnology/Resources/image-20231006140150745.png new file mode 100644 index 00000000..b93b7674 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006140150745.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006151018957.png b/Docs/02-EngineTechnology/Resources/image-20231006151018957.png new file mode 100644 index 00000000..8467bb31 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006151018957.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006192226000.png b/Docs/02-EngineTechnology/Resources/image-20231006192226000.png new file mode 100644 index 00000000..e8bb8f35 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006192226000.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006201624396.png b/Docs/02-EngineTechnology/Resources/image-20231006201624396.png new file mode 100644 index 00000000..e8bb8f35 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006201624396.png differ diff --git a/Docs/02-EngineTechnology/Resources/image-20231006201742745.png b/Docs/02-EngineTechnology/Resources/image-20231006201742745.png new file mode 100644 index 00000000..109a47e7 Binary files /dev/null and b/Docs/02-EngineTechnology/Resources/image-20231006201742745.png differ diff --git "a/Docs/04-UnrealEngine/0.\345\237\272\347\241\200\347\274\226\347\250\213.md" "b/Docs/04-UnrealEngine/0.\345\237\272\347\241\200\347\274\226\347\250\213.md" new file mode 100644 index 00000000..33c95479 --- /dev/null +++ "b/Docs/04-UnrealEngine/0.\345\237\272\347\241\200\347\274\226\347\250\213.md" @@ -0,0 +1,1486 @@ +--- +comments: true +--- + +# Unreal Engine C++ 编程文档 + +## 前言 + +**Unreal Engine** 是一个由 C++ 编写的 强大引擎,但由于 **构建工具(UBT)** 和 **反射编译器(UHT)** 的存在 ,导致它有着独立于C++标准的语法,因此网友们也戏称它为 **U++** 。 + +不仅语法上存在一定差异,Unreal Engine下的开发流程也与平常的C++开发流程大相径庭。举例来说,STL标准库就像是一个工具箱(Toolkit), **We use it to develop** ,而 Unreal Eninge 则更像是一个开发平台(Platform), **We develop based on it** 。 + +当下U++教程和书籍并不多,其中对开发者而言作用较大的系列文章有: + +- [大象无形:虚幻引擎程序设计浅析](https://book.douban.com/subject/27033749/) +- [UE4开发指南 - Gamedev Guide](https://ikrima.dev/ue4guide/) +- [循迹研究室 - 查利鹏](https://imzlp.com/) +- [剖析虚幻渲染体系 - 向往](https://www.cnblogs.com/timlly/p/13512787.html) +- [InsideUE - 大钊](https://zhuanlan.zhihu.com/p/22813908) + +Unreal Engine 匮乏的文档和晦涩的代码对C++初学者而言几乎就是一道天堑,不可逾越,这是因为Unreal Engine这种工业级别的项目有着许多的开发人员,它的代码体量庞大,且迭代很快,想要整理和归纳它的发展过程是一件非常困难的事情。 + +笔者翻阅了一些U++的视频教程,比较零碎,大多是一些披着UE皮的C++基础教程,介绍的内容有限,且存在比较多的盲区,学习收益并不是很大。 + +> 如果有发现好的教程,小伙伴也可以在评论区推荐~ + +想要上手Unreal Engine,往往需要具备中大型C++项目的工程能力,可以明确的说, **它并不适合C++初学者** + +如果你是一个还在上学的学生,机缘巧合下发现了Unreal Engine这么一个神奇的东西,被它绚丽的画面效果所吸引,心血来潮想要学习U++开发,那么你应该思考一下,自己除了会刷题和写论文,是否还具备以下技能: + +- 熟悉 C++ 的工程构建 +- 对引擎中的各个子模块有一定认知,具备中大型项目源码的阅读能力 + +如果没有,那么笔者建议你再静下心来好好打磨一下,而不是硬着头皮直接上 + +基础得经历长年打磨才能变得牢靠,而技术也是一点一点地慢慢积累,才能一层一层地逐渐搭高,不能想着什么弯道超车,听别人吹嘘谁谁谁,随随便便折腾几下,技术就多么多么牛,但实际上, **在工业界,没有不努力的天才** ,至少在笔者所敬重的人中,无一例外,而技术并无强弱之分,大多时候无非是 **闻道有先后,术业有专攻** + +拿笔者的经历来说,笔者在三年前学过的一段时间U++,当时是把官方文档给过了一遍,然后照着这个教程敲了一遍: + +- https://docs.unrealengine.com/5.2/zh-CN/unreal-engine-cpp-programming-tutorials/ + +过完之后迷迷糊糊的,感觉很多代码莫名其妙,有太多知识雾区,干脆放弃了,倒不如去学一些自己能够看得见摸得着的东西,所以去看了 Qt,CMake,OpenGL,一些图形学知识和数字信号的东西,然后做了它: + +- [音频可视化图形引擎—Specinker](https://blog.csdn.net/qq_40946921/article/details/104124455/) + +当我能熟练掌握这些技能之后,再回头去看 Unreal Engine 时,哇,一片明朗~ + +当看到 **UBT(Unreal Build Tool)** 的时候,哦,原来它的用途跟CMake一样,用来管理工程构建,可以利用它去做这些操作: + +- 给构建目标添加 **依赖库(Library** ), **包含目录(Include Directories)** , **宏定义(Definitions)** +- 设置编译器配置 +- 执行构建脚本 + +瞄见 **UHT(Unreal Header Tool)** 的时候,我想到了Qt的Moc(Meta Object Compiler),工作流程就是扫描代码头文件,得到一些信息,然后生成新的代码文件添加到构建目标中,使用它来实现UE的代码反射,而通过反射,可以实现基于类型的控件,基于对象的面板,还能自行组织反射数据,从而实现像蓝图这样的可视化编程脚本。 + +遇到 **Slate** ,根本都不需要文档,就能猜到: + +- **FSlateApplication** 是一个全局单例,它是所有UI的调度中心,它里面可以获取和设置整个应用的状态,并提供一些有用的操作 +- **SWindow** 是顶层窗口,它持有跨平台的窗口实例(FGenericWindow),提供窗口相关的配置和操作。 +- **SWidget** 用来划分窗口区域,处理自身区域内的交互和绘制事件,看到它的第一反应是查阅它有哪些可供设置的属性,哪些可以覆盖的事件虚函数,以及怎么处理它PaintEvent + +再把一些基础的GUI概念给映射过来: + +- **窗口的基本状态** :激活(Active),焦点(Focus),可见(Visible),模态(Modal),变换(Transform) +- **布局策略及相关概念** : + - 盒式布局(HBox,VBox),流式布局(Flow),网格布局(Grid),锚式布局(Anchors),重叠布局(Overlap),画布(Canvas) + - 内边距(Margin),外边距(Padding),间距(Spacing),对齐方式(Alignment) +- **样式管理** :图标(Icon),风格(Style),画刷(Brush) +- **字体管理** :字体类型(Font Family),文本宽度测量(Font Measure) +- **尺寸计算** :控件尺寸计算策略 +- **交互事件** :鼠标,键盘,拖拽,焦点事件,事件处理机制,鼠标捕获 +- **绘制事件** :绘制元素,区域裁剪 +- **基本控件** :标签(Label),按钮(Button),复选框(Check),组合框(Combo),滑动条(Slider),滚动条(Scroll Bar),文本框(Text),对话框(Dialog),颜色选取(Color),菜单栏(Menu Bar),菜单(Menu),状态栏(Status Bar),滚动面板(Scroll ),堆栈(切换)面板(Stack/Switcher),列表面板(List),树形面板(Tree),停靠窗口(Dock),... +- **国际化** :文本本地化翻译(Localization) + +Ok,我发现我也能用Slate实现自己能想象到任何界面效果。 + +看到 **RHI(Rendering Hardware Interface)** ,我能联想到它对应OpenGL/Vulkan里的什么操作,如果要做自定义扩展,那无非就是照猫画虎。 + +碰上 **Niagara** ,知道GPU粒子只不过是通过Compute Pipeline在交互链上模拟粒子运动,利用原子操作和间接渲染去做粒子回收,最终将粒子数据作为实例化数据绑定到粒子渲染器的参数上,从而渲染出粒子特效,而Niagara中的Module,也只是在组织Compute Shader的代码和流水线资源,正因为知道粒子系统的实现原理,所以对Niagara的流程和性能非常敏感。 + +看到 **Wolrd** , **Actor** , **Component** , **Controller** ...,才恍然意识到Gameplay架构原来可以使用这么多结构去划分代码的职责,比我这半吊子水平做的好太多了~ + +诸如此类,可以说,当我放弃跟着一些文档和教程随波逐流之后,反倒是那些在通用框架上建立起来的知识体系,让我在回头审视 Unreal Engine 时,有了 ”不同寻常“ 的思考维度 —— 一个引擎里面应该有什么东西,哦,Unreal Engine里面也有,且做的很好。 + +因此,如果你还不具备中大型C++的工程能力,笔者建议你可以学习一下Qt,或者一些开源的,轻量级的,基础文档齐全的引擎,如果能尝试自己搭建一个出来,那就更好了~ + +在建立起扎实的基础知识体系之后,后续学习的重点主要是 **思路** + +本文旨在阐述基础开发的主干路线,仅仅只是一个开发者文档,并非教程。 + +## 工程结构 + +一个标准的UE工程的文件结构如下: + +![image-20230517115452232](Resources/image-20230517115452232-16848129576282.png) + +- `Config`:存放项目中的各类配置文件(GameConfig,EngineConfig,EditorConfig,PluginConfig...) +- `Content`:存放项目的资源文件 +- `Saved`:暂存目录,项目开发过程中生成的文件一般都位于此,包括日志,崩溃记录,烘焙,本地编辑器配置等 +- `Source`:存放项目的源码文件 +- `MyProj.uproject`:项目工程文件 + +### uproject + +`*.uproject` 存储了工程的一些基本信息,它的初始结构如下: + +```json +{ + "FileVersion": 3, + "EngineAssociation": "5.2", + "Category": "", + "Description": "", + "Modules": [ + { + "Name": "MyProj", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + } + ] +} +``` + +该文件的关键参数有: + +- `EngineAssociation`:引擎的版本 +- `Modules`:该工程拥有的代码模块 +- `Plugins`:该工程开启的 **内置** 插件 + +这些参数虽然可以手动修改,但大多时候,在UE的编辑器上进行变更会更加安全和任意。 + +#### 切换引擎版本 + +在`*.uproject` 的右键菜单下,可以切换当前工程的引擎版本: + +![image-20230523140419952](Resources/image-20230523140419952.png) + +![image-20230523140518199](Resources/image-20230523140518199.png) + +如果引擎没有出现在选择框的下拉列表中,则需要到下方目录使用 **UnrealVersionSelector-Win64-Shipping.exe** 进行注册: + +![image-20230523140739431](Resources/image-20230523140739431.png) + +#### 开启内置插件 + +在引擎中,开启内置插件后,编辑器会自动在`*.uproject` 文件的`Plugins`下追加新的插件条目: + +![image-20230523115125388](Resources/image-20230523115125388.png) + +#### 添加工程模块 + +**模块(Modules)** 是 **虚拟引擎(UE)** 的软件架构的基础构建块。它们在独立的代码单元中封装了一体的编程工具、运行时功能、库或其他功能。 + +使用模块化编程可以带来以下好处: + +- 模块会强制实施良好的代号分离,可用于封装功能和隐藏代号的内部成分。 +- 模块编译为单独的编译元。这意味着,只有已经更改的模块才需要编译,更大项目的编译时间会显着缩短。 +- 模块在依赖性图表中链接在一起,并且只允许允许实际使用的代码包包含头文件,以符合[Include What You Use (IWYU)](https://docs.unrealengine.com/5.2/zh-CN/include-what-you-use-iwyu-for-unreal-engine-programming)标准。这意味着,你的项目中未使用的模块将安全地排除在编辑中之外。 +- 你可以控制在运行时任何时候加载和加载实体的模块。 这样一来,可以管理哪些系统可用并活跃,从而优化项目的性能。 +- 你可以基于特定条件(例如,项目是为哪个平台编写的),在你的项目中纳入或排除模块。 + +所有项目和插件在默认情况下都有自己的 **主模块** + +在UE编辑器主面板-`工具(Tools)`- `调试(Debug)` - `模块(Modules)`,可以看到当前工程开启的所有模块: + +![image-20230523115940276](Resources/image-20230523115940276.png) + +关于模块的创建,请参阅: + +- 虚幻引擎模块:https://docs.unrealengine.com/5.2/zh-CN/unreal-engine-modules/ +- 创建Gameplay模块:https://docs.unrealengine.com/5.2/zh-CN/how-to-make-a-gameplay-module-in-unreal-engine/ + +除了阅读文档,你还必须了解C++项目构建的一些基础概念: + +- `*.Build.cs` 是UE模块的构建文件,它里面定义了该模块的构建规则,其中就包含了包含路径,依赖库,编译选项等。(类似于CMake的CMakeLists.txt) + +一个基本的`*.Build.cs` 结构如下: + +```C# +using UnrealBuildTool; +using System.IO; // for Path +public class ModuleName : ModuleRules +{ + public ModuleName(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + // ... add other public dependencies that you statically link with here ... + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + // ... add private dependencies that you statically link with here ... + } + ); + } +} +``` + +- UE的构建工具(UBT)会根据`*.Build.cs` 生成项目的 **工程文件** (VS工程的`*.sln`文件) ,所以当修改了`*.Build.cs`的内容或者模块的代码文件结构,需要使用UBT重新生成工程文件,这样IDE才能对变更的代码和依赖关系进行分析。 +- 包含路径和依赖库是C++工程构建的基本概念。 + - 包含路径用于增加头文件定义的搜索路径。 + - 依赖库用于给当前模块 **链接(Link)** 外部模块的实现。 + +假如项目中存在一个C++头文件 `D:/Unreal Projects/MyProj/Source/MyProj/MyHeader.h`,要想使用它里面的代码定义,可以直接使用: + +```c++ +#include "D:/Unreal Projects/MyProj/Source/MyProj/MyHeader.h" +``` + +如果在`*.Build.cs` 中的`IncludePaths`添加了`"D:/Unreal Projects/MyProj/Source/MyProj"`,那么就可以换成: + +``` c++ +#include "MyHeader.h" +``` + +如果想在当前模块使用其他模块的代码,只需要在`*.Build.cs` 的`DependencyModuleNames`中添加目标模块即可 + +> 如果没有做这一步,在编译的时候会报链接错误(Link Error),这个时候只需要找到所使用结构的代码文件,通过IDE找到该文件位于哪个`*.Build.cs` 下,将它的模块名字添加到 `DependencyModuleNames` 中就能解决这个问题。 + +#### Public和Private的差异 + +简单而言,Public代表可传递,Private代表仅自己使用。 + +假设有三个模块A,B,C,他们的代码文件结构如下: + +- 文件夹A + - A.Build.cs + - Public文件夹 + - a.h + - Private文件夹 + - A.h +- 文件夹B + - B.Build.cs + - Public文件夹 + - b.h + - Private文件夹 + - B.h + +- 文件夹C + - C.Build.cs + - Public文件夹 + - c.h + - Private文件夹 + - C.h + +`*.Build.cs` 的伪代码如下: + +```C# +public class A(ReadOnlyTargetRules Target) : base(Target){ +} + +public class B(ReadOnlyTargetRules Target) : base(Target){ + PrivateDependencyModuleNames.AddRange( new string[] { "A"}); +} + +public class C(ReadOnlyTargetRules Target) : base(Target){ + PublicDependencyModuleNames.AddRange( new string[] { "B"}); +} +``` + +如果在 **c.h** 中去使用模块A和B的文件,将导致以下结果: + +```c++ +#include "C.h" +//【编译报错0】,Private文件夹并不是模块C的搜索路径。 + +#include "b.h" +//编译正常,UE会将模块自身的 Public目录 自动加入到 Build.cs 的 PublicIncludePaths 中,又由于 模块C的公有依赖 中加入了 模块B +//所以 模块B的PublicIncludePaths 也会传递给 模块C,因此模块C中可以正常访问B中的Public目录 + + +#include "B.h" +//【编译报错1】,模块C无法访问到模块B的Private目录 + +#include "A.h" +//【编译报错2】,由于 模块C的公有依赖 中加入了 模块B,而 模块B 却只是在私有依赖 中加入了 模块A +//因此B中可以正常使用A中的Public内容,但C不能使用A + +#include "a.h" +//【编译报错3】,模块C无法访问到模块A的任何内容 + +``` + +要去除以上报错,可以将`*.Build.cs` 的结构改为: + +``` C# +public class A(ReadOnlyTargetRules Target) : base(Target){ + PublicIncludePaths.AddRange( new string[] { "Private"}); //修复【编译报错3】,将模块A的Private文件夹添加到公有包含路径中 +} + +public class B(ReadOnlyTargetRules Target) : base(Target){ + PublicDependencyModuleNames.AddRange( new string[] { "A"}); //修复【编译报错2,3】,将模块A作为模块B的公用依赖,表示可传递依赖 + PublicIncludePaths.AddRange( new string[] { "Private"}); //修复【编译报错1】,将模块B的Private文件夹添加到公有包含路径中 +} + +public class C(ReadOnlyTargetRules Target) : base(Target){ + PublicDependencyModuleNames.AddRange( new string[] { "B"}); + PublicIncludePaths.AddRange( new string[] { "Private"}); //修复【编译报错0】,将模块C的Private文件夹添加到公有包含路径中 +} +``` + +看完上面的例子,理解Public和Private的区别并不难: + +- Public 表示可传递 +- Private 表示仅当前使用 + +读者可能会好奇,直接统一使用Public不就好了,能省去很多操作,那为什么不全用Public呢?是因为它也会带来问题: + +- 定义冲突:当引入的模块出现重叠的定义(类,函数,全局变量)时,会导致编译报错。 +- 编译缓慢:假如模块B引入了模块A,当模块A的代码变动时,也会触发B模块的重编译,所以大量的非必要依赖会严重拖垮编译的速度,例如`#include "CoreMinimal.h"`也会导致这样的问题。 + +为了让模块不会出现编译冲突的风险和编译缓慢的问题,所以在写模块的时候,应尽可能使用Private,当模块需要对外传递时,才考虑用Public。 + +## 对象系统 + +UObject 是 UE 对象系统的基类,它的结构层次如下: + +![image-20220622175331091](Resources/image-20220622175331091.png) + +它主要由三级结构构成,这三级结构有着不同的职责: + +- **UObjectBase** :数据层,定义了UObject全部的基础数据,其中包括: + + - **ObjectFlags** :Object的各类标识 + - **InternalIndex** :UE中所有Object的指针都存放在一个数组中,这里记录index便于在数组中快速定位,主要用于GC + - **ClassPrivate** :Object的元类型—UClass + - **NamePrivate** :Object的名称 + - **OuterPrivate** :持有该Object的对象(与序列化有关, **跟生命周期无关** ) + +- **UObjectBaseUtility** :数据接口层,提供了很多数据层的处理接口。 + + ![image-20220622180328342](Resources/image-20220622180328342.png) + +- **UObject** :功能层,提供了大量对象系统的基本函数接口。 + +在 **UObjectBase** 的构造函数中有这样的代码: + +```c++ +UObjectBase::UObjectBase(UClass* InClass, EObjectFlags InFlags, EInternalObjectFlags InInternalFlags, UObject *InOuter, FName InName) +: ObjectFlags (InFlags) +, InternalIndex (INDEX_NONE) +, ClassPrivate (InClass) +, OuterPrivate (InOuter) +{ + check(ClassPrivate); + // Add to global table. + AddObject(InName, InInternalFlags); +} +``` + +其中AddObject会将新建的UObject地址,存放到全局变量 **GUObjectArray** 中,它位于:`Runtime\CoreUObject\Private\UObject\UObjectHash.cpp` + +```c++ +// Global UObject array instance +FUObjectArray GUObjectArray; +``` + +**FUObjectArray** 的定义位于`Runtime\CoreUObject\Public\UObject\UObjectArray.h`,它内部主要用于管理所有的Object,簇,GC,其主要数据成员如下: + +```c++ +class FUObjectArray{ + //typedef TStaticIndirectArrayThreadSafeRead TUObjectArray; + typedef FChunkedFixedUObjectArray TUObjectArray; + + // note these variables are left with the Obj prefix so they can be related to the historical GObj versions + + /** First index into objects array taken into account for GC. */ + int32 ObjFirstGCIndex; + /** Index pointing to last object created in range disregarded for GC. */ + int32 ObjLastNonGCIndex; + /** Maximum number of objects in the disregard for GC Pool */ + int32 MaxObjectsNotConsideredByGC; + + /** If true this is the intial load and we should load objects int the disregarded for GC range. */ + bool OpenForDisregardForGC; + /** Array of all live objects. */ + TUObjectArray ObjObjects; + /** Synchronization object for all live objects. */ + mutable FCriticalSection ObjObjectsCritical; + /** Available object indices. */ + TArray ObjAvailableList; + + /** + * Array of things to notify when a UObjectBase is created + */ + TArray UObjectCreateListeners; + /** + * Array of things to notify when a UObjectBase is destroyed + */ + TArray UObjectDeleteListeners; +}; +``` + +UE支持监听UObject的创建与销毁: + +```c++ +class FUObjectCreateListener +{ +public: + virtual ~FUObjectCreateListener() {} + virtual void NotifyUObjectCreated(const class UObjectBase *Object, int32 Index)=0; + virtual void OnUObjectArrayShutdown()=0; +}; + +class FUObjectDeleteListener +{ +public: + virtual ~FUObjectDeleteListener() {} + virtual void NotifyUObjectDeleted(const class UObjectBase *Object, int32 Index)=0; + virtual void OnUObjectArrayShutdown() = 0; +}; +``` + +开发者可自定义派生这两类监听器,并调用 **GUObjectArray** 的以下函数,从而达到全局监控或修改UObject的目的(UnLua就是这么做的): + +```c++ +class FUObjectArray{ + void AddUObjectCreateListener(FUObjectCreateListener* Listener); + void RemoveUObjectCreateListener(FUObjectCreateListener* Listener); + void AddUObjectDeleteListener(FUObjectDeleteListener* Listener); + void RemoveUObjectDeleteListener(FUObjectDeleteListener* Listener); +}; +``` + +还有一个关键结构是 **FUObjectHashTables** ,它是一个单例类,存放了大量用于快速查找的映射,其代码结构如下: + +```c++ +struct FHashBucket{ + void *ElementsOrSetPtr[2]; //可能是UObjectBase* ,也可能是 TSet* +}; + +template +class TBucketMap : private TMap{ + //... +}; + +class FUObjectHashTables +{ + /** 线程锁 */ + FCriticalSection CriticalSection; +public: + + TBucketMap Hash; //Id到Object的映射 + TMultiMap HashOuter; //Id到父对象ID的映射 + + TBucketMap ObjectOuterMap; //Object 到 子对象集合 的映射 + TBucketMap ClassToObjectListMap; //UClass 到 其所有实例 的映射 + TMap > ClassToChildListMap; //UClass 到 其派生Class 的映射 + TAtomic ClassToChildListMapVersion; + + TBucketMap PackageToObjectListMap; //包 到 对象集 的映射 + + TMap ObjectToPackageMap; //对象 到 包 的映射 + + static FUObjectHashTables& Get() + { + static FUObjectHashTables Singleton; + return Singleton; + } + //... +}; +``` + +### 创建 + +一个简单的UObject类定义如下: + +``` c++ +#pragma once + +#include "UObject/Object.h" +#include "CustomObject.generated.h" //如果存在 #include "{文件名}.generated.h" ,UE则会使用UHT生成该文件的反射数据 + +UCLASS() +class UCustomObject :public UObject { + GENERATED_BODY() //GENERATED_BODY() 反射的入口宏,UHT会生成该宏的定义,里面定义了一些结构塞到UCustomObject的类定义中 +public: + UCustomObject() {} +}; +``` + +创建UObject的对象实例一般情况下会使用函数`NewObject<>()`,例如`NewObject()`,它的完整函数定义如下: + +```c++ +/** + * Convenience template for constructing a gameplay object + * + * @param Outer the outer for the new object. If not specified, object will be created in the transient package. + * @param Class the class of object to construct + * @param Name the name for the new object. If not specified, the object will be given a transient name via MakeUniqueObjectName + * @param Flags the object flags to apply to the new object + * @param Template the object to use for initializing the new object. If not specified, the class's default object will be used + * @param bCopyTransientsFromClassDefaults if true, copy transient from the class defaults instead of the pass in archetype ptr (often these are the same) + * @param InInstanceGraph contains the mappings of instanced objects and components to their templates + * @param ExternalPackage Assign an external Package to the created object if non-null + * + * @return a pointer of type T to a new object of the specified class + */ +template< class T > +FUNCTION_NON_NULL_RETURN_START + T* NewObject(UObject* Outer, + const UClass* Class, + FName Name = NAME_None, + EObjectFlags Flags = RF_NoFlags, + UObject* Template = nullptr, + bool bCopyTransientsFromClassDefaults = false, + FObjectInstancingGraph* InInstanceGraph = nullptr, + UPackage* ExternalPackage = nullptr) +{ + if (Name == NAME_None) + { + FObjectInitializer::AssertIfInConstructor(Outer, TEXT("NewObject with empty name can't be used to create default subobjects (inside of UObject derived class constructor) as it produces inconsistent object names. Use ObjectInitializer.CreateDefaultSubobject<> instead.")); + } + +#if DO_CHECK + // Class was specified explicitly, so needs to be validated + CheckIsClassChildOf_Internal(T::StaticClass(), Class); +#endif + + FStaticConstructObjectParameters Params(Class); + Params.Outer = Outer; + Params.Name = Name; + Params.SetFlags = Flags; + Params.Template = Template; + Params.bCopyTransientsFromClassDefaults = bCopyTransientsFromClassDefaults; + Params.InstanceGraph = InInstanceGraph; + Params.ExternalPackage = ExternalPackage; + return static_cast(StaticConstructObject_Internal(Params)); +} +``` + +关于Object的创建,这里需要注意几点: + +- **Outer 跟 Object 的生命周期没有任何关系** :Outer 参数是为了指明当前对象 **在存储上** 隶属于哪个父对象,它跟资产序列化有关。 + +- **可以通过UClass来创建对象** :`NewObject()`等价于`NewObject(GetTransientPackage(), UCustomObject::StaticClass())` + +### 销毁 + +UE中通过垃圾回收来管理UObject对象的生命周期,这是一种基于对象引用 定时定量 的回收方式。 + +其核心机制可以简单理解为: + +- 根对象不会被释放 +- 如果一个对象不会被释放,那么它所引用的对象也不会被释放 + +在UE中,可以使用以下函数手动销毁对象: + +- `bool UObject::ConditionalBeginDestroy()`:直接销毁对象,而不用等待GC +- `void UObject::MarkAsGarbage()`:标记为垃圾,下一次GC时会被释放 + +作为开发者,主要需要了解以下几点: + +- 如何让一个对象不能被释放? +- 如何构建对象间的引用? +- 如何配置和使用GC? + +UE中想让一个对象不可被释放,主要有四种途径: + +- 在`NewObject`时,使用`EObjectFlags::RF_Standalone`标识,该标识能保证创建的对象在编辑器中一直不会被GC回收(详见`GarbageCollection.h line28`) + +- 使用函数 `void UObject::AddToRoot()` 可以将对象加入到根对象集合中,从而让对象不会被释放,使用`void UObject::RemoveFromRoot()`可以将对象从根对象集合中移除。 + +- 派生 **FGCObject** 类,它被广泛用于UE中各个模块的对象生命周期管理:![image-20230523170954045](Resources/image-20230523170954045.png) + + UE在执行GC时,会调用 **所有FGCObject对象** 的 `virtual void AddReferencedObjects( FReferenceCollector& Collector )` 函数, **Collector** 是一个操作入口,添加到 **Collector** 中的对象在本次GC中不会被释放。 + + 而 **FGCObject** 的构造函数中,会把该对象注册到UE全局的 FGCObject对象列表 中,相当于派生了FGCObject类的对象就可以作为一个根对象 + + 因此开发者只需要在自己原生的结构定义上,增加`public FGCObject`,并派生它的相关函数: + + ```c++ + class FMyManager : public FGCObject { + virtual void AddReferencedObjects(FReferenceCollector& Collector) override { + //Collector.AddReferencedObject(...) + //.. + } + virtual FString GetReferencerName() const override{ return TEXT("FMyManager"); } + }; + ``` + +- 使用智能指针 **TStrongObjectPtr** 来存储对象,它的本质其实依旧是使用了 **FGCObject** ,详见: + - [UE4的智能指针 UObject相关](https://zhuanlan.zhihu.com/p/371851019) + +UE中主要是通过反射来构建对象间的引用,比如这样的代码: + +``` c++ +UCLASS() +class UCustomObject :public UObject { +public: + UCustomObject() {} + + UPROPERTY() + UObject* Prop = nullptr; +}; + +int main() { + auto A = NewObject(); + auto B = NewObject(); + A->Prop = B; + A->AddToRoot(); + // Engine Loop + return 0; +} +``` + +- 由于`对象A`被添加到了根对象集合,因此它不会被释放,又因为UE根据反射确定了`对象A`的`Prop属性`引用了`对象B`,因此B也不会被释放 + +可以在项目配置中对GC的参数细节进行调整: + +![image-20230523174457486](Resources/image-20230523174457486.png) + +在C++中,可以使用如下代码在当前帧手动执行GC: + +```C++ +bool bForceGarbageCollectionPurge = true; //是否为全量GC +GEngine->ForceGarbageCollection(bForceGarbageCollectionPurge); +``` + +另外,需要特别注意的是,UE的 对象系统 和 反射系统 相辅相成,由于一些原因导致UObject对象 **没有使用析构函数** 去完成资源的释放逻辑,所以当需要实现析构逻辑时,请覆写UObject的虚函数 `virtual void BeginDestroy()` + +### 常用操作 + +创建唯一对象名: + +``` c++ +FName MakeUniqueObjectName(UObject* Parent, + const UClass* Class, + FName InBaseName/*=NAME_None*/, + EUniqueObjectNameOptions Options /*= EUniqueObjectNameOptions::None*/) +``` + +迭代: + +``` c++ + // 迭代当前所有类型 +for(UClass* Class : TObjectRange()) { +} + + // 迭代当前所有对象实例 +for(UObject* Object : TObjectRange()) { +} + + // 迭代UObject的所有属性 +for(TFieldIterator PropertyIterator(UObject::StaticClass()); PropertyIterator; ++PropertyIterator){ +} + +// 获取所有派生自UObject的UClass +TArray DerivedClasses +GetDerivedClasses(UObject::StaticClass(),DerivedClasses,true); +``` + +`Runtime\CoreUObject\Public\UObject\UObjectHash.h`提供了一些静态方法来管理和访问 **FUObjectHashTables** 的数据 + +``` c++ +UObject* StaticFindObjectFastInternal(const UClass* Class, const UObject* InOuter, FName InName, bool ExactClass = false, bool AnyPackage = false, EObjectFlags ExclusiveFlags = RF_NoFlags, EInternalObjectFlags ExclusiveInternalFlags = EInternalObjectFlags::None); + +UObject* StaticFindObjectFastExplicit(const UClass* ObjectClass, FName ObjectName, const FString& ObjectPathName, bool bExactClass, EObjectFlags ExcludeFlags = RF_NoFlags); + +COREUOBJECT_API void GetObjectsWithOuter(const class UObjectBase* Outer, TArray& Results, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None); + +COREUOBJECT_API void ForEachObjectWithOuterBreakable(const class UObjectBase* Outer, TFunctionRef Operation, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None); + +inline void ForEachObjectWithOuter(const class UObjectBase* Outer, TFunctionRef Operation, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None) +{ + ForEachObjectWithOuterBreakable(Outer, [Operation](UObject* Object) { Operation(Object); return true; }, bIncludeNestedObjects, ExclusionFlags, ExclusionInternalFlags); +} + +COREUOBJECT_API class UObjectBase* FindObjectWithOuter(const class UObjectBase* Outer, const class UClass* ClassToLookFor = nullptr, FName NameToLookFor = NAME_None); + +COREUOBJECT_API void GetObjectsWithPackage(const class UPackage* Outer, TArray& Results, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None); + +COREUOBJECT_API void ForEachObjectWithPackage(const class UPackage* Outer, TFunctionRef Operation, bool bIncludeNestedObjects = true, EObjectFlags ExclusionFlags = RF_NoFlags, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None); + +COREUOBJECT_API void GetObjectsOfClass(const UClass* ClassToLookFor, TArray& Results, bool bIncludeDerivedClasses = true, EObjectFlags ExcludeFlags = RF_ClassDefaultObject, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None); + +COREUOBJECT_API void ForEachObjectOfClass(const UClass* ClassToLookFor, TFunctionRef Operation, bool bIncludeDerivedClasses = true, EObjectFlags ExcludeFlags = RF_ClassDefaultObject, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None); + +COREUOBJECT_API void ForEachObjectOfClasses(TArrayView ClassesToLookFor, TFunctionRef Operation, EObjectFlags ExcludeFlags = RF_ClassDefaultObject, EInternalObjectFlags ExclusionInternalFlags = EInternalObjectFlags::None); + +/*获取UClass所有派生类的UClass*/ +COREUOBJECT_API void GetDerivedClasses(const UClass* ClassToLookFor, TArray& Results, bool bRecursive = true); + +COREUOBJECT_API TMap> GetAllDerivedClasses(); + +COREUOBJECT_API bool ClassHasInstancesAsyncLoading(const UClass* ClassToLookFor); + +void HashObject(class UObjectBase* Object); + +void UnhashObject(class UObjectBase* Object); + +void HashObjectExternalPackage(class UObjectBase* Object, class UPackage* Package); + +void UnhashObjectExternalPackage(class UObjectBase* Object); + +UPackage* GetObjectExternalPackageThreadSafe(const class UObjectBase* Object); + +UPackage* GetObjectExternalPackageInternal(const class UObjectBase* Object); +``` + +`Editor\UnrealEd\Public\ObjectTools.h`中的命名空间ObjectTools也提供了非常多 **编辑器下** 安全操作Object的方法,此外相似的还有 **ThumbnailTools** , **AssetTools** ... + +``` c++ +namespace ObjectTools +{ + UNREALED_API bool IsObjectBrowsable( UObject* Obj ); + + UNREALED_API void DuplicateObjects( const TArray& SelectedObjects, const FString& SourcePath = TEXT(""), const FString& DestinationPath = TEXT(""), bool bOpenDialog = true, TArray* OutNewObjects = NULL ); + + UNREALED_API UObject* DuplicateSingleObject(UObject* Object, const FPackageGroupName& PGN, TSet& InOutPackagesUserRefusedToFullyLoad, bool bPromptToOverwrite = true, TMap, TSoftObjectPtr>* DuplicatedObjects = nullptr); + + UNREALED_API FConsolidationResults ConsolidateObjects( UObject* ObjectToConsolidateTo, TArray& ObjectsToConsolidate, bool bShowDeleteConfirmation = true ); + UNREALED_API FConsolidationResults ConsolidateObjects(UObject* ObjectToConsolidateTo, TArray& ObjectsToConsolidate, TSet& ObjectsToConsolidateWithin, TSet& ObjectsToNotConsolidateWithin, bool bShouldDeleteAfterConsolidate, bool bWarnAboutRootSet = true); + UNREALED_API void CompileBlueprintsAfterRefUpdate(const TArray& ObjectsConsolidatedWithin); + + UNREALED_API void CopyReferences( const TArray< UObject* >& SelectedObjects ); // const + + UNREALED_API void ShowReferencers( const TArray< UObject* >& SelectedObjects ); // const + + UNREALED_API void ShowReferenceGraph( UObject* ObjectToGraph ); + + UNREALED_API void ShowReferencedObjs( UObject* Object, const FString& CollectionName = FString(), ECollectionShareType::Type ShareType = ECollectionShareType::CST_Private); + + UNREALED_API void SelectActorsInLevelDirectlyReferencingObject( UObject* RefObj ); + + UNREALED_API void SelectObjectAndExternalReferencersInLevel( UObject* Object, const bool bRecurseMaterial ); + + UNREALED_API void AccumulateObjectReferencersForObjectRecursive( UObject* Object, TArray& Referencers, const bool bRecurseMaterial ); + + bool ShowDeleteConfirmationDialog ( const TArray& ObjectsToDelete ); + + UNREALED_API void CleanupAfterSuccessfulDelete ( const TArray& ObjectsDeletedSuccessfully, bool bPerformReferenceCheck = true ); + + UNREALED_API int32 DeleteObjects( const TArray< UObject* >& ObjectsToDelete, bool bShowConfirmation = true, EAllowCancelDuringDelete AllowCancelDuringDelete = EAllowCancelDuringDelete::AllowCancel); + + UNREALED_API int32 DeleteObjectsUnchecked( const TArray< UObject* >& ObjectsToDelete ); + + UNREALED_API int32 DeleteAssets( const TArray& AssetsToDelete, bool bShowConfirmation = true ); + + UNREALED_API bool DeleteSingleObject( UObject* ObjectToDelete, bool bPerformReferenceCheck = true ); + + UNREALED_API int32 ForceDeleteObjects( const TArray< UObject* >& ObjectsToDelete, bool ShowConfirmation = true ); + + UNREALED_API void ForceReplaceReferences(UObject* ObjectToReplaceWith, TArray& ObjectsToReplace); + UNREALED_API void ForceReplaceReferences(UObject* ObjectToReplaceWith, TArray& ObjectsToReplace, TSet& ObjectsToReplaceWithin); + + UNREALED_API void AddExtraObjectsToDelete(TArray< UObject* >& ObjectsToDelete); + + UNREALED_API bool ComposeStringOfReferencingObjects( TArray& References, FString& RefObjNames, FString& DefObjNames ); + + UNREALED_API void DeleteRedirector(UObjectRedirector* Redirector); + + UNREALED_API bool GetMoveDialogInfo(const FText& DialogTitle, UObject* Object, bool bUniqueDefaultName, const FString& SourcePath, const FString& DestinationPath, FMoveDialogInfo& InOutInfo); + + UNREALED_API bool RenameObjectsInternal( const TArray& Objects, bool bLocPackages, const TMap< UObject*, FString >* ObjectToLanguageExtMap, const FString& SourcePath, const FString& DestinationPath, bool bOpenDialog ); + + UNREALED_API bool RenameSingleObject(UObject* Object, FPackageGroupName& PGN, TSet& InOutPackagesUserRefusedToFullyLoad, FText& InOutErrorMessage, const TMap< UObject*, FString >* ObjectToLanguageExtMap = NULL, bool bLeaveRedirector = true); + + UNREALED_API void AddLanguageVariants( TArray& OutObjects, TMap< UObject*, FString >& OutObjectToLanguageExtMap, USoundWave* Wave ); + + UNREALED_API bool RenameObjects( const TArray< UObject* >& SelectedObjects, bool bIncludeLocInstances, const FString& SourcePath = TEXT(""), const FString& DestinationPath = TEXT(""), bool bOpenDialog = true ); + + UNREALED_API FString SanitizeObjectName(const FString& InObjectName); + + UNREALED_API FString SanitizeObjectPath(const FString& InObjectPath); + + UNREALED_API FString SanitizeInvalidChars(const FString& InText, const FString& InvalidChars); + + UNREALED_API FString SanitizeInvalidChars(const FString& InText, const TCHAR* InvalidChars); + + UNREALED_API void SanitizeInvalidCharsInline(FString& InText, const TCHAR* InvalidChars); + + UNREALED_API void GenerateFactoryFileExtensions( const UFactory* InFactory, FString& out_Filetypes, FString& out_Extensions, TMultiMap& out_FilterIndexToFactory ); + + UNREALED_API void GenerateFactoryFileExtensions( const TArray& InFactories, FString& out_Filetypes, FString& out_Extensions, TMultiMap& out_FilterIndexToFactory ); + + UNREALED_API void AppendFactoryFileExtensions( UFactory* InFactory, FString& out_Filetypes, FString& out_Extensions ); + + UNREALED_API void AppendFormatsFileExtensions(const TArray& InFormats, FString& out_FileTypes, FString& out_Extensions); + + UNREALED_API void AssembleListOfExporters(TArray& OutExporters); + + UNREALED_API void TagInUseObjects( EInUseSearchOption SearchOption, EInUseSearchFlags InUseSearchFlags = EInUseSearchFlags::None); + + UNREALED_API TSharedPtr OpenPropertiesForSelectedObjects( const TArray& SelectedObjects ); + + UNREALED_API void RemoveDeletedObjectsFromPropertyWindows( TArray& DeletedObjects ); + + UNREALED_API bool IsAssetValidForPlacing(UWorld* InWorld, const FString& ObjectPath); + + UNREALED_API bool IsClassValidForPlacing(const UClass* InClass); + + UNREALED_API bool IsClassRedirector( const UClass* Class ); + + UNREALED_API bool AreObjectsOfEquivalantType( const TArray& InProposedObjects ); + + UNREALED_API bool AreClassesInterchangeable( const UClass* ClassA, const UClass* ClassB ); + + UNREALED_API void GatherObjectReferencersForDeletion(UObject* InObject, bool& bOutIsReferenced, bool& bOutIsReferencedByUndo, FReferencerInformationList* OutMemoryReferences = nullptr, bool bInRequireReferencingProperties = false); + + UNREALED_API void GatherSubObjectsForReferenceReplacement(TSet& InObjects, TSet& ObjectsToExclude, TSet& OutObjectsAndSubObjects); +}; +``` + +## 反射系统 + +反射是现代程序开发中一个兴起的概念,它旨在以 Meta 的角度去审视代码,将代码中的枚举、类、结构、函数...作为运行时可访问甚至操作的资产 **,** 它本质上是C++代码的自省。 + +关于反射的概念以及基本实现,详见: + +- [现代图形引擎入门指南(五)— 宏 模板 反射](https://zhuanlan.zhihu.com/p/600456225) + +在UE中,一个相对完整的反射标记示例如下: + +```c++ +/*CustomStruct.h*/ +#pragma once +#include "UObject/Object.h" +#include "CustomStruct.generated.h" + +UCLASS() +class UCustomClass :public UObject { + GENERATED_BODY() +public: + UCustomClass() {} +public: + UFUNCTION() + static int Add(int a, int b) { + UE_LOG(LogTemp, Warning, TEXT("Hello World")); + return a + b; + } + +private: + UPROPERTY(meta = (Keywords = "This is keywords")) + int SimpleValue = 123; + + UPROPERTY() + TMap ComplexValue = { {"Key0",0},{"Key1",1} }; +}; + +UENUM() +enum class ECustomEnum : uint8 { + One UMETA(DisplayName = "This is 0"), + Two UMETA(DisplayName = "This is 1"), + Tree UMETA(DisplayName = "This is 2") +}; + +USTRUCT() +struct FCustomStruct +{ + GENERATED_BODY() + + UPROPERTY() + int Value = 123; +}; + +//Interface的固定结构定义 +UINTERFACE() +class UCustomInterface :public UInterface { + GENERATED_BODY() +}; +class ICustomInterface +{ + GENERATED_BODY() +public: + UFUNCTION() + virtual void SayHello() {} +}; +``` + +对于上述宏标记的结构,UE都会创建相应的数据结构来保存它的元数据 + +- **UCLASS()** :UClass,存储类的描述信息 +- **USTRUCT()** :UScriptStruct,存储结构体的描述信息 +- **UENUM()** :UEnum,存储枚举的描述信息 +- **UPROPERTY()** :FProperty,存储属性的描述信息 +- **UFUNCTION()** :UFunction,存储函数的描述信息 +- **UINTERFACE()** :无 + +在这些宏的括号中,可以添加一些参数,供给相关系统使用,具体的配置请参阅: + +- [Reflection System in Unreal Engine | Unreal Engine 5.2 Documentation](https://docs.unrealengine.com/5.2/en-US/reflection-system-in-unreal-engine/) + - [Objects in Unreal Engine | Unreal Engine 5.2 Documentation](https://docs.unrealengine.com/5.2/en-US/objects-in-unreal-engine/) + - [UFunctions in Unreal Engine | Unreal Engine 5.2 Documentation](https://docs.unrealengine.com/5.2/en-US/ufunctions-in-unreal-engine/) + - [Unreal Engine UProperties | Unreal Engine 5.2 Documentation](https://docs.unrealengine.com/5.2/en-US/unreal-engine-uproperties/) + - [Structs in Unreal Engine | Unreal Engine 5.2 Documentation](https://docs.unrealengine.com/5.2/en-US/structs-in-unreal-engine/) + - [Interfaces in Unreal Engine | Unreal Engine 5.2 Documentation](https://docs.unrealengine.com/5.2/en-US/interfaces-in-unreal-engine/) + - [Metadata Specifiers in Unreal Engine | Unreal Engine 5.2 Documentation](https://docs.unrealengine.com/5.2/en-US/metadata-specifiers-in-unreal-engine/) + +- 其他:https://www.unrealengine.com/en-US/blog/unreal-property-system-reflection + +这里有一个相对完整的反射使用示例: + +```c++ +UClass* MetaClassFromGlobalStatic = StaticClass(); +UClass* MetaClassFromMemberStatic = UCustomClass::StaticClass(); //根据类型通过静态方法获取UClass +UCustomClass* Instance = NewObject(); +UClass* MetaClassFromInstance = Instance->GetClass(); //根据实例获取UClass,当实例指针退化为父类时依然有效 +UClass* SuperMetaClass = MetaClassFromInstance->GetSuperClass(); //获取父类的UClass +bool IsCustomClass = MetaClassFromInstance->IsChildOf(UCustomClass::StaticClass()); //判断继承关系 + +UFunction* FuncAdd = MetaClassFromInstance->FindFunctionByName("Add"); //获取函数 +UFunction* SuperFunc = FuncAdd->GetSuperFunction(); //获取父函数,此时为null + +/*调用函数*/ +int A = 5, B = 10; +uint8* Params = (uint8*)FMemory::Malloc(FuncAdd->ReturnValueOffset); +FMemory::Memcpy((void*)Params, (void*)&A, sizeof(int)); +FMemory::Memcpy((void*)(Params + sizeof(A)), (void*)&B, sizeof(int)); +FFrame Frame(Instance, FuncAdd, Params, 0, FuncAdd->ChildProperties); +int Result; +FuncAdd->Invoke(Instance, Frame, &Result); +UE_LOG(LogTemp, Warning, TEXT("FuncAdd: a + b = %d"), Result); + +/*覆盖函数*/ +FuncAdd->SetNativeFunc(execCallSub); +Frame = FFrame(Instance, FuncAdd, Params, 0, FuncAdd->ChildProperties); +FuncAdd->Invoke(Instance, Frame, &Result); +UE_LOG(LogTemp, Warning, TEXT("FuncSub: a - b = %d"), Result); + +/*读写属性信息*/ +FIntProperty* SimpleProperty = FindFProperty(UCustomClass::StaticClass(), "SimpleValue"); +int* SimplePropertyPtr = SimpleProperty->ContainerPtrToValuePtr(Instance); //根据复合结构的地址,得到实际属性的地址 +*SimplePropertyPtr = 789; + +FMapProperty* ComplexProperty = FindFProperty(UCustomClass::StaticClass(), "ComplexValue"); +TMap* ComplexPropertyPtr = ComplexProperty->ContainerPtrToValuePtr>(Instance); +ComplexPropertyPtr->FindOrAdd("Key02", 2); + +bool HasBlueprintVisible = SimpleProperty->HasAllPropertyFlags(EPropertyFlags::CPF_BlueprintVisible); +FString MetaData = SimpleProperty->GetMetaData("Keywords"); + +/*枚举相关*/ +UEnum* MetaEnum = StaticEnum(); +UEnum* MetaEnumFromValueName = nullptr; +UEnum::LookupEnumName("EMetaEnum::One", &MetaEnumFromValueName); //根据枚举元素的名称获取 +for (int i = 0; i < MetaEnum->NumEnums(); i++) { + UE_LOG(LogTemp, Warning, TEXT("%s : %d - %s"), *(MetaEnum->GetNameByIndex(i).ToString()), MetaEnum->GetValueByIndex(i), *MetaEnum->GetMetaData(TEXT("DisplayName"), i)); +} + +/*UStruct相关*/ +UStruct* MetaStructFromGlobalStatic = StaticStruct(); +UStruct* MetaStructFromMemberStatic = FCustomStruct::StaticStruct(); +FCustomStruct* StructInstance = new FCustomStruct; +``` + +### CDO(Class Default Object) + +UE的反射系统为UObject提供了一个名为 **Class Default Object** 的机制,即:每个类有一个默认对象 + +因为 CDO 的存在,使得每个Class都拥有了一个 **全局单例** + +通过如下函数可以获取 CDO: + +``` c++ +UObject* UClass::GetDefaultObject(bool bCreateIfNeeded = true) const +{ + if (ClassDefaultObject == nullptr && bCreateIfNeeded) // 不存在则新建 + { + UE_TRACK_REFERENCING_PACKAGE_SCOPED((GetOutermost()), PackageAccessTrackingOps::NAME_CreateDefaultObject); + const_cast(this)->CreateDefaultObject(); + } + return ClassDefaultObject; +} +``` + +UE也提供了一些便捷的方法获取: + +```c++ +const UObject* ConstCDO = GetDefault(); //获取CDO的Cosnt指针 +UObject* MutableCDO = GetMutableDefault(); //获取可变的CDO指针 +``` + +UE在`NewObject`的时候会以CDO作为模板,因此你可以修改CDO的属性值来调整 新对象属性 的初始值。 + +UE的细节面板也会以CDO作为参照,查看当前对象的哪些属性与CDO存在差异: + +![image-20230523200403812](Resources/image-20230523200403812.png) + +此外,CDO还与UE的Config机制紧密关联: + +```c++ +UCLASS(config = CustomSettings, defaultconfig) +class UCustomSettings : public UObject { + GENERATED_BODY() +public: + UPROPERTY(EditAnywhere, Config) + FString ConfigValue; +}; +``` + +因为 CDO 全局单例的特性,可以用它来存储一些配置,通过在`UCLASS`和`UPROPERTY`中增加Config相关的标识,使用如下代码可以将相关配置存储到项目的工程目录下: + +```c++ +CDO->SaveConfig(); +``` + +如果想让配置文件进一步显示在编辑器的项目配置中,可以在插件的启动时机使用如下代码注册配置文件: + +```c++ +ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings"); +if (SettingsModule) { + auto Settings = GetMutableDefault(); + SettingsModule->RegisterSettings("Project", + TEXT("CustomSection"), + TEXT("CustomSettingsName"), + LOCTEXT("CustomSettingsName", "CustomSettingsName"), + LOCTEXT("CCustomSettingsTooltip", "Custom Settings Tooltip"), + Settings); +} +``` + +还需要注意的是,CDO也会执行UObject的构造函数,因此在做一些损耗比较高的操作时,需要考虑CDO是否也需要执行: + +``` c++ +UCustomObject::UCustomObject(const FObjectInitializer& ObjectInitializer){ + if (HasAnyFlags(EObjectFlags::RF_ClassDefaultObject)) { //判断当前对象是否是CDO + } +} +``` + +### 蓝图(Blueprint) + +Unreal Engine 反射中最强大的点在于:开发者不仅可以通过 扫描代码 生成 反射数据,甚至还 **可以自行创建和组织反射的数据结构** + +这是一个使用代码去构造UClass的示例: + +```c++ +DEFINE_FUNCTION(execIntToString) //自定义本地函数 +{ + P_GET_PROPERTY(FIntProperty, IntVar); //获取参数 + P_FINISH; //获取结束 + *((FString*)RESULT_PARAM) = FString::FromInt(IntVar); //设置返回值 +} +``` + +``` c++ +UClass* CustomClass = NewObject(GetTransientPackage(),"CustomClass"); //自定义类 +CustomClass->ClassFlags |= CLASS_EditInlineNew; //设置类标识 +CustomClass->SetSuperStruct(UObject::StaticClass()); //设置父类 + +FIntProperty* IntProp = new FIntProperty(CustomClass, TEXT("IntProp"), EObjectFlags::RF_NoFlags);//创建Int属性 +FStrProperty* StringProp = new FStrProperty(CustomClass, TEXT("StringProp"), EObjectFlags::RF_NoFlags); + +UPackage* CoreUObjectPkg = FindObjectChecked(nullptr, TEXT("/Script/CoreUObject")); //创建Vector结构体属性 +UScriptStruct* VectorStruct = FindObjectChecked(CoreUObjectPkg, TEXT("Vector")); +FStructProperty* VectorProp = new FStructProperty(CustomClass, "VectorProp", RF_NoFlags); +VectorProp->Struct = VectorStruct; + +CustomClass->ChildProperties = IntProp; //CustomClass->ChildProperties 是一个链表节点,通过链表来链接属性 +IntProp->Next = StringProp; +StringProp->Next = VectorProp; + +UFunction* CustomFucntion = NewObject(CustomClass, "IntToString", RF_Public); +CustomFucntion->FunctionFlags = FUNC_Public | FUNC_Native; //本地函数指的是本地C++函数,否则是指蓝图脚本中的函数代码 + +FIntProperty* IntParam = new FIntProperty(CustomFucntion, "IntParam", EObjectFlags::RF_NoFlags); +IntParam->PropertyFlags = CPF_Parm; //用作函数参数 + +FStrProperty* StringReturnParam = new FStrProperty(CustomFucntion, "StringReturnParam", EObjectFlags::RF_NoFlags); +StringReturnParam->PropertyFlags = CPF_Parm | CPF_ReturnParm; // 用作函数返回值(必须包含CPF_Parm) + +CustomFucntion->ChildProperties = IntParam; // 函数返回值必须是最后的节点,用于计算后续的参数偏移 +IntParam->Next = StringReturnParam; + +CustomFucntion->SetNativeFunc(execIntToString); // 设置本地函数 +CustomFucntion->Bind(); +CustomFucntion->StaticLink(true); // 链接所有属性,此操作会生成Property的内存大小、偏移值.. +CustomFucntion->Next = CustomClass->Children; // 将Function添加Class Children链表的头部,以便Field访问 +CustomClass->Children = CustomFucntion; + +CustomClass->AddFunctionToFunctionMap(CustomFucntion, CustomFucntion->GetFName()); //将自定义函数添加到自定义类的函数表中 + +CustomClass->Bind(); +CustomClass->StaticLink(true); // 链接所有属性 +CustomClass->AssembleReferenceTokenStream(); // 关联属性的GC,完成自定义类的构建 + +UObject* CDO = CustomClass->GetDefaultObject(); + +// 对CDO中的属性进行赋值 +void* IntPropPtr = IntProp->ContainerPtrToValuePtr(CDO); +IntProp->SetPropertyValue(IntPropPtr, 789); + +FVector Vector(1, 2, 3); +void* VectorPropPtr = VectorProp->ContainerPtrToValuePtr(CDO); +VectorProp->CopyValuesInternal(VectorPropPtr, &Vector, 1); + +// 调用自定义函数 +int InputParam = 12345; +UFunction* Func = CustomClass->FindFunctionByName("IntToString"); +uint8* Params = (uint8*)FMemory::Malloc(Func->ReturnValueOffset); //分配参数缓冲区 +FMemory::Memcpy((void*)Params, (void*)&InputParam, sizeof(int)); +FFrame Frame(CDO, Func, Params, 0, Func->ChildProperties); +FString ReturnParam; +Func->Invoke(CDO, Frame, &ReturnParam); +``` + +而在这之后,我们甚至可以使用自己创建的UClass来创建UObject: + +```c++ +UObject* CustomObject = NewObject(GetTransientPackage(), CustomClass); +``` + +而UE的蓝图,不过是对上面的操作进行了一些包装,并提供了相应的编辑器。 + +UE的蓝图对应的数据结构是 **UBlueprint** ,我们在蓝图编辑器上的绝大部分操作(属性、函数操作,节点编辑,类设置...),基本上都是在修改 **UBlueprint** 的参数: + +![img](Resources/image-20220928175121797-16735895312067.png) + +使用如下接口可以创建一个蓝图: + +```c++ +static UBlueprint* FKismetEditorUtilities::CreateBlueprint( + UClass* ParentClass, + UObject* Outer, + const FName NewBPName, + enum EBlueprintType BlueprintType, + TSubclassOf BlueprintClassType, + TSubclassOf BlueprintGeneratedClassType, + FName CallingContext = NAME_None); +``` + +参数意义如下: + +- **ParentClass** :父类,生成蓝图将继承该Class的属性和方法 + +- **Outer** :生成蓝图的Outer(持有者) + +- **NewBPname** :生成蓝图的名称 + +- **BlueprintType** :蓝图的类型、可以是如下值: + + ``` c++ + enum EBlueprintType + { + /** Normal blueprint. */ + BPTYPE_Normal UMETA(DisplayName="Blueprint Class"), + /** Blueprint that is const during execution (no state graph and methods cannot modify member variables). */ + BPTYPE_Const UMETA(DisplayName="Const Blueprint Class"), + /** Blueprint that serves as a container for macros to be used in other blueprints. */ + BPTYPE_MacroLibrary UMETA(DisplayName="Blueprint Macro Library"), + /** Blueprint that serves as an interface to be implemented by other blueprints. */ + BPTYPE_Interface UMETA(DisplayName="Blueprint Interface"), + /** Blueprint that handles level scripting. */ + BPTYPE_LevelScript UMETA(DisplayName="Level Blueprint"), + /** Blueprint that serves as a container for functions to be used in other blueprints. */ + BPTYPE_FunctionLibrary UMETA(DisplayName="Blueprint Function Library"), + + BPTYPE_MAX, + }; + ``` + +- **BlueprintClassType** :蓝图类的基础模板,默认可以使用`UBlueprint::StaticClass()`,它可以用来自定义蓝图配置 + +- **BlueprintGeneratedClassType** :蓝图生成类的基础模板,默认可以使用`UBlueprintGeneratedClass::StaticClass()`,,它可以用来自定义生成策略 + +- **CallingContext** :调用上下文,默认为空 + +创建蓝图的逻辑可以简单看做是: + +``` c++ +UBlueprint* FKismetEditorUtilities::CreateBlueprint( + UClass* ParentClass, + UObject* Outer, + TSubclassOf BlueprintClassType, + TSubclassOf BlueprintGeneratedClassType) +{ + UBlueprint* NewBP = NewObject(Outer,*BlueprintClassType); //根据BlueprintClassType创建实例 + NewBP->ParentClass = ParentClass; + + UBlueprintGeneratedClass* NewGenClass = //根据BlueprintGeneratedClassType创建实例 + NewObject(NewBP->GetOutermost(),*BlueprintGeneratedClassType); + + NewGenClass->SetSuperStruct(ParentClass) //设置NewGenClass的父类为ParentClass,这样就能访问其属性和方法 + + NewBP->GeneratedClass = NewGenClass; //将新生成的UClass赋值给NewBP->GeneratedClass +} +``` + +当点击蓝图编辑器的编译按钮时 + +![image-20230523203638600](Resources/image-20230523203638600.png) + +实际会调用以下函数: + +``` c++ +static void FKismetEditorUtilities::CompileBlueprint(UBlueprint* BlueprintObj, + EBlueprintCompileOptions CompileFlags = EBlueprintCompileOptions::None, + class FCompilerResultsLog* pResults = nullptr ); +``` + +该函数的目标是依据 蓝图编辑器 的 `属性列表` 和 `逻辑图表` 再加上 `ParentClass` 来生成 UBlueprint 的 **成员变量GeneratedClass** + +通常情况下,在C++侧去使用蓝图的步骤如下: + +``` c++ +UClass * BlueprintGenClass = LoadObject(nullptr, "/Game/NewBlueprint.NewBlueprint_C"); //获取蓝图中的生成类 +UObject* Instance = NewObject(GetTransientPackage(),Blueprint->GeneratedClass); //根据类来创建对象实例 + +UFunction* Func = Blueprint->GeneratedClass->FindFunctionByName("FuncName"); // 在类反射数据中查找名称为[FuncName]的函数 +Instance->ProcessEvent(Func, nullptr); // 调用蓝图中定义的函数 + +FProperty* Prop = FindFProperty(Blueprint->GeneratedClass, "IntProp");// 在类反射数据中查找名称为[IntProp]的属性 +int value = 0; +Prop->GetValue_InContainer(Instance,&value); //获取属性 +Prop->SetValue_InContainer(Instance,&value); //设置属性 +``` + +使用`/Game/NewBlueprint.NewBlueprint_C` 正是得到 `UBlueprint 的成员变量GeneratedClass` ,其中`/Game/NewBlueprint`是Package路径,`NewBlueprint_C`是`成员变量GeneratedClass`的 对象名称。 + +对于 **UBlueprint** 生成的 **UClass** ,可以访问 **UClass** 的成员变量 `ClassGeneratedBy`,来获取生成它的 **UBlueprint** ,也可以使用函数: + +```c++ +static UBlueprint* UBlueprint::GetBlueprintFromClass(const UClass* InClass); +``` + +理解蓝图的原理之后,它的使用并不困难,下面是一个使用代码来创建蓝图并打开编辑器的示例: + +``` c++ +UClass* ParentClass = NewObject(); //创建ParentClass +ParentClass->SetSuperStruct(UObject::StaticClass()); +ParentClass->ClassFlags = CLASS_Abstract | CLASS_MatchedSerializers | CLASS_Native | CLASS_ReplicationDataIsSetUp | CLASS_RequiredAPI | CLASS_TokenStreamAssembled | CLASS_HasInstancedReference | CLASS_Constructed; + +//为Class添加事件函数 +UFunction* BPEvent = NewObject(ParentClass, "BlueprintEvent", RF_Public | RF_Transient); +BPEvent->FunctionFlags = FUNC_Public | FUNC_Event | FUNC_BlueprintEvent; +BPEvent->Bind(); +BPEvent->StaticLink(true); + +BPEvent->Next = ParentClass->Children; //将函数添加到Parent的Field中 +ParentClass->Children = BPEvent; +ParentClass->AddFunctionToFunctionMap(BPEvent, "BlueprintEvent"); + +ParentClass->Bind(); +ParentClass->StaticLink(true); +ParentClass->AssembleReferenceTokenStream(true); + +UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint( //创建蓝图 + ParentClass, + GetTransientPackage(), + "NewBP", + EBlueprintType::BPTYPE_Normal, + UBlueprint::StaticClass(), + UBlueprintGeneratedClass::StaticClass()); + +int32 NodePositionY = 0; + +//在图表中创建事件节点 +FKismetEditorUtilities::AddDefaultEventNode(NewBP, NewBP->UbergraphPages[0], "BlueprintEvent", ParentClass, NodePositionY); + +//当事件不存在时会创建自定义事件 +FKismetEditorUtilities::AddDefaultEventNode(NewBP, NewBP->UbergraphPages[0], "CustomEvent", ParentClass, NodePositionY); + +//打开蓝图编辑器 +FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked("Kismet"); +BlueprintEditorModule.CreateBlueprintEditor(EToolkitMode::Standalone, nullptr, NewBP); +``` + +![image-20220928202723812](Resources/image-20220928202723812-16735895507339.png) + +## 资产 + +UE的资产管理也是建立在对象系统之上,UObject支持序列化和反序列化,因此任何一个UObject都可以作为资产(Asset)。 + +UE的序列化接口如下: + +``` c++ +class UObject{ + /** + * Handles reading, writing, and reference collecting using FArchive. + * This implementation handles all FProperty serialization, but can be overridden for native variables. + */ + virtual void Serialize(FArchive& Ar); +}; +``` + +UObject会自动序列化 `非Transient` 的反射属性,也可以覆写`Serialize`函数,使用`FArchive`增加非反射的数据。 + +使用 **FObjectReader** 、 **FObjectWriter** 可以将UObject和ByteArray互转。 + +关于序列化,详见: + +- [知乎 - UE序列化介绍及源码剖析](https://zhuanlan.zhihu.com/p/617464719) + +UE中的资产不等同于物理文件,UE中物理文件的管理,实际上是依靠于 **UPackage** + +**UPackage** 对应着 Content 目录下的`*.uasset`和`*.umap`文件 + +UE中创建资产时,首先会创建一个`UPackage`作为实际的物理文件映射,然后使用 `NewObject` 创建实际资产,此时会指定它的`Outer`为`Package`,当保存`Package`时,会将该`Package`持有的所有`UObject`都序列化到物理文件中,这个过程看上去就像是: + +``` c++ +// 创建一个新Package,/Game/CustomPackage为包路径,其中/Game代表工程的Content目录 +UPackage* CustomPackage = CreatePackage(nullptr, TEXT("/Game/CustomPackage")); + +// 创建一个新的资产,可以是任意UObject(这里使用的UObject是抽象类,会有警告) +UObject* CustomInstance = NewObject(CustomPackage, UObject::StaticClass(), + TEXT("CustomInstance"), + EObjectFlags::RF_Public | EObjectFlags::RF_Standalone); + +// 获取Package的本地磁盘路径 +FPackagePath PackagePath = FPackagePath::FromPackageNameChecked(CustomPackage->GetName()); +const FString PackageFileName = PackagePath.GetLocalFullPath(); + +// 将包存储到磁盘上 +const bool bSuccess = UPackage::SavePackage(CustomPackage, + CustomInstance, + RF_Public | RF_Standalone, + *PackageFileName, + GError, nullptr, false, true, SAVE_NoError); +``` + +该操作会在工程的Content目录下创建一个`CustomPackage.uasset`文件 + +![image-20230524105904811](Resources/image-20230524105904811.png) + +我们可以通过如下代码去加载磁盘上的包和资产对象: + +``` C++ +UPackage* LoadedPackage = LoadPackage(nullptr, TEXT("/Game/CustomPackage"), LOAD_None); +UObject* LoadedAsset = LoadObject(nullptr,TEXT("/Game/CustomPackage.CustomInstance")); +``` + +如果想让该资产能够在UE编辑器上被创建和预览,则需要: + +- 派生 **UFactory** ,定义资产对象的生成工厂 +- 派生 **FAssetTypeActions_Base** ,定义资产在编辑器上的预览,菜单等行为 + +关于资产管理的更多细节,请查阅: + +- [知乎 - UE4的资源管理](https://zhuanlan.zhihu.com/p/357904199) + +### 常用操作 + +批量处理资产: + +``` c++ +TArray AssetDataList; +IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); +AssetRegistry.GetAssetsByClass(UStaticMesh::StaticClass()->GetFName(), AssetDataList); +for (auto AssetData : AssetDataList) { + UObject* Asset = AssetData.GetAsset(); + //Do something... +} +``` + +离线加载地图: + +```C++ +FString MapLoadCommand = FString::Printf(TEXT("MAP LOAD FILE=%s TEMPLATE=0 SHOWPROGRESS=0 FEATURELEVEL=3"), *MapPath); +FPackagePath Path = FPackagePath::FromLocalPath(*MapPath); +GEditor->Exec(nullptr, *MapLoadCommand, *GError); +FlushAsyncLoading(); +UPackage* MapPackage = FindPackage(nullptr, *Path.GetPackageName()); +UWorld* World = MapPackage ? UWorld::FindWorldInPackage(MapPackage) : nullptr; +``` + +离线保存资产: + +``` c++ +FEditorFileUtils::SaveDirtyPackages(false,true,true,true); +``` + +等待资产编译结束: + +``` c++ +FAssetCompilingManager::Get().FinishAllCompilation(); //蓝图,材质,纹理,距离场,Niagara... +``` + +## 委托 + +UE 中使用 **委托(Delegate)** 的概念来进行对象间的通信,它可以以通用且类型安全的方式调用C++函数。 + +> 如果你熟知Qt的话,Delegate其实对应Qt的信号槽(Signal-Slot)中的Signal,只不过Delegate相比Signal更强大,在使用上也变得更复杂。 +> +> 它最大的槽点在于命名,如果也叫Signal,相信可以更容易勾起开发者的联想而使之采用正确的使用方式。 + +以下是一个使用Delegate的场景(使用Signal命名的伪代码): + +```C++ +class Student{ + Signal nameChanged(string OldName, string NewName); +public: + void setName(string inName){ + string oldName = mName; + mName = inName; + nameChanged.Emit(oldName,mName); + } +private: + string mName; +}; + +class Tearcher{ +public: + void OnStudentNameChanged(string OldName, string NewName){ + // do something + } +}; + +class Class{ +protected: + void connectEveryone(){ + for(auto Student:mStudents){ + Student.nameChanged.Connect(mTeacher,&Tearcher::OnStudentNameChanged); + } + } +private: + Teacher mTeacher; + TArray mStudents; +}; +``` + +该示例中`connectEveryone`函数完成的功能可以看做是:"老师"在班级里面告诉学生,如果你们改名字了,请通知我。 + +这里没有直接修改Teacher和Student的结构,而是在第三方(班级)中进行二者关系的绑定,从而达到代码的解耦。 + +如果不使用委托,实现相同的功能,可能会写出这样丑陋的代码: + +``` c++ +class Student{ +public: + Student(Class* InClass) //需要传递上一级逻辑域的指针 + : mClass(InClass){} + + void setName(string inName){ + string oldName = mName; + mName = inName; + + //不安全的链式调用,且上下文跨度较大的调用会让后续的维护和迭代变得困难 + mClass->mTeacher->OnStudentNameChanged(OldName,mName); + } +private: + string mName; + Class* mClass +}; + +class Tearcher{ +public: + void OnStudentNameChanged(string OldName, string NewName){ + // do something + } +}; + +class Class{ + friend class Student; +private: + Teacher mTeacher; + TArray mStudents; +}; +``` + +关于UE委托的使用,详见: + +- [UE4中的委托及实现原理 - 鹅厂程序小哥](https://zhuanlan.zhihu.com/p/575671003) + diff --git "a/Docs/04-UnrealEngine/1.Slate\345\274\200\345\217\221.md" "b/Docs/04-UnrealEngine/1.Slate\345\274\200\345\217\221.md" new file mode 100644 index 00000000..fd5fbefd --- /dev/null +++ "b/Docs/04-UnrealEngine/1.Slate\345\274\200\345\217\221.md" @@ -0,0 +1,1294 @@ +--- +comments: true +--- + +Slate是UE中提供的UI框架,它的核心理念如下: + +- [Slate 架构|虚幻引擎5.2文档 ](https://docs.unrealengine.com/5.2/zh-CN/understanding-the-slate-ui-architecture-in-unreal-engine/) + +关于UI的一些基础概念,可以了解: + +- [现代图形引擎入门指南(七)— GUI](https://zhuanlan.zhihu.com/p/605656730) + +## 基础 + +在UE中,使用Slate,需要了解三个核心结构: + +- **FSlateApplication** :全局单例,所有UI的调度中心。 +- **SWindow** :顶层窗口,持有跨平台窗口的实例(FGenericWindow),提供窗口相关的配置和操作。 +- **SWidget** :小部件,划分窗口区域,处理自身区域内的交互和绘制事件。 + +### 基本使用 + +在UE中,一个简单的Slate使用示例如下: + +```C++ +auto Window = SNew(SWindow) //创建窗口 + .ClientSize(FVector2D(600, 600)) //设置窗口大小 + [ //填充窗口内容 + SNew(SHorizontalBox) //创建水平盒子 + + SHorizontalBox::Slot() //添加子控件插槽 + [ + SNew(STextBlock) //创建文本框 + .Text(FText::FromString("Hello")) //设置文本框内容 + ] + + SHorizontalBox::Slot() //添加子控件插槽 + [ + SNew(STextBlock) //创建文本框 + .Text_Lambda([](){ //设置文本框内容 + return FText::FromString("Slate"); + }) + ] + ]; +FSlateApplication::Get().AddWindow(Window, true); //注册该窗口,并立即显示 +``` + +Slate的代码风格如下: + +- Slate 控件的类命名一般以 `S`开头 +- 可以通过以下函数来快速构建Slate控件: + - `SNew( WidgetType, ... )`:通用的构造方式 + - `SAssignNew( ExposeAs, WidgetType, ... )`:构造完成后,把控件赋值给`ExposeAs` + - `SArgumentNew( InArgs, WidgetType, ... )`:使用参数集进行构造 + +- 在构造的代码表达式中,可以通过一些函数来设置控件的构造参数,由于这些函数都会返回控件自身的引用,因此可以进行链式调用 + - 可以通过`operatpr .` 调用函数来设置控件的属性和事件 + - 对于 **SPanel** 的子类,可以使用`operator +SPanelType::Slot`添加子控件的插槽 + - 对于 **SCompoundWidget** `和 ` **SPanel::Slot** ,可以使用`operator []`来填充子控件 + - 对于Slate的属性和事件,可以通过多种方式绑定,比如上面的`.Text(...)`设置的是静态值,还可以通过绑定函数来动态获取属性值: + - `Text_Lambda(...)` + - `Text_Raw(...)` + - `Text_Static(...)` + - `Text_UObject(...)` + +控件开发中,我们可以依靠一些经验(合理即存在),以及IDE的辅助来完成代码的编写。 + +![image-20230530153625676](Resources/image-20230530153625676.png) + +Slate可以同时作为 Game UI 和 Editor UI + +在Editor中,可以通过如下方式来添加UI: + +```C++ +auto Window = SNew(SWindow) //必须具有一个顶层窗口 + .Title(FText::FromString("CustomWindow")) //设置窗口标题 + .ClientSize(FVector2D(600, 600)) //设置窗口大小 + [ //填充窗口内容 + SNew(SSpacer) //自身控件,这里是一个空白填充 + ]; + +//方法1:注册该窗口,并立即显示 +FSlateApplication::Get().AddWindow(Window, true); + +//方法2:注册该窗口,不显示,手动调用ShowWindow来显示 +FSlateApplication::Get().AddWindow(Window, false); //注册该窗口,并立即显示 +Window->ShowWindow(); +``` + +![image-20230530161318939](Resources/image-20230530161318939.png) + +也可以通过`DockTab`的方式来添加窗口: + +``` c++ +// 注册Tab页面的生成器 +FGlobalTabmanager::Get()->RegisterTabSpawner(FName("CustomTab"),FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& Args){ + return SNew(SDockTab) + [ + SNew(SSpacer) + ]; +})); +FGlobalTabmanager::Get()->TryInvokeTab(FTabId("CustomTab")); //尝试激活Tab页面 +``` + +![image-20230530161340764](Resources/image-20230530161340764.png) + +在Game中,可以通过以下方式来添加UI: + +``` C++ +GEngine->GameViewport->AddViewportWidgetContent( + SNew(SSpacer) +); +``` + +GameViewport还有其他接口可用: + +``` c++ +class UGameViewportClient : public UScriptViewportClient, public FExec +{ + virtual void AddViewportWidgetContent( TSharedRef ViewportContent, const int32 ZOrder = 0 ); + virtual void RemoveViewportWidgetContent( TSharedRef ViewportContent ); + virtual void AddViewportWidgetForPlayer(ULocalPlayer* Player, TSharedRef ViewportContent, const int32 ZOrder); + virtual void RemoveViewportWidgetForPlayer(ULocalPlayer* Player, TSharedRef ViewportContent); + void RemoveAllViewportWidgets(); + void RebuildCursors(); +}; +``` + +> 详见:https://docs.unrealengine.com/5.2/en-US/using-slate-in-game-in-unreal-engine/ + +### 基本概念 + +对于UI开发,需要了解以下的基础概念: + +- **窗口的基本状态** :激活(Active),焦点(Focus),可见(Visible),模态(Modal),变换(Transform) +- **布局策略及相关概念** : + - 盒式布局(HBox,VBox),流式布局(Flow),网格布局(Grid),锚式布局(Anchors),重叠布局(Overlap),画布(Canvas) + - 内边距(Padding),外边距(Margin),间距(Spacing),对齐方式(Alignment) +- **样式管理** :图标(Icon),风格(Style),画刷(Brush) +- **字体管理** :字体类型(Font Family),文本宽度测量(Font Measure) +- **尺寸计算** :控件尺寸计算策略 +- **交互事件** :鼠标,键盘,拖拽,焦点事件,事件处理机制,鼠标捕获 +- **绘制事件** :绘制元素,区域裁剪 +- **基本控件** :标签(Label),按钮(Button),复选框(Check),组合框(Combo),滑动条(Slider),滚动条(Scroll Bar),文本框(Text),对话框(Dialog),颜色选取(Color),菜单栏(Menu Bar),菜单(Menu),状态栏(Status Bar),滚动面板(Scroll ),堆栈(切换)面板(Stack/Switcher),列表面板(List),树形面板(Tree)... +- **国际化** :文本本地化翻译(Localization) + +并了解 UI 框架中有哪些全局数据和操作,在UE中,可以使用输入`Get`,`Set`,根据IDE提示简单过一遍: + +> 这也体现了遵循一定编码规范所带来的好处 + +![image-20230530165544358](Resources/image-20230530165544358.png) + +### SWidget + +**SWidget** 是 Slate UI开发过程中,开发者 接触频率最多 的类型,它是所有UI控件的基类 + +对于开发者而言,需要了解它的基本骨架,知道哪些属性可以调整: + +``` c++ +TAttribute EnabledState, //开启状态,指定是否能够与控件交互,如果禁用,则显示为灰色 +TAttribute Visibility, //可见性策略 +TSharedPtr ToolTip, //提示框控件 +TAttribute ToolTipText, //提示框文本内容 +TAttribute > Cursor, //鼠标样式 +float RenderOpacity, //渲染透明度 +TAttribute> Transform, //渲染变换 +TAttribute TransformPivot, //渲染变换中心 +FName Tag, //标签 +bool ForceVolatile, //强制UI失效 +EWidgetClipping Clipping, //裁剪策略 +EFlowDirectionPreference FlowPreference, //UI流向 +TOptional AccessibleData, //存储数据 +TArray> MetaData //元数据 +``` + +> 详见:https://docs.unrealengine.com/5.2/zh-CN/slate-ui-widget-examples-for-unreal-engine/ + +哪些函数可以调用: + +![image-20230530181510368](Resources/image-20230530181510368.png) + +哪些函数可以进行覆写(Override): + +![image-20230530183325442](Resources/image-20230530183325442.png) + +### SWindow + +**SWindow** 继承自 **SCompoundWidget** ,这样做只是为了让 **SWindow** 能够使用像 **SWidget** 一样的代码风格,它具有以下属性: + +``` c++ +EWindowType Type; //窗口类型 +FWindowStyle Style; //窗口样式 +FText Title; //窗口标题 +float InitialOpacity; //初始透明度 + +FVector2D ScreenPosition //窗口坐标 +FVector2D ClientSize; //窗口客户区域尺寸 +TOptional MinWidth; //最小宽度 +TOptional MinHeight; //最小高度 +TOptional MaxWidth; //最大宽度 +TOptional MaxHeight; //最大高度 +FMargin LayoutBorder; //窗口内容的边距 +FMargin UserResizeBorder; //调整窗口区域时的响应距离 + +EAutoCenter AutoCenter; //居中策略 +ESizingRule SizingRule; //窗口尺寸处理策略 +FWindowTransparency SupportsTransparency; //窗口透明度策略 +EWindowActivationPolicy ActivationPolicy; //激活处理策略 + +bool IsInitiallyMaximized; //初始时显示为最大化 +bool IsInitiallyMinimized; //初始时显示为最小化 +bool IsPopupWindow; //是否是 Pop up 窗口(无任务栏图标) +bool IsTopmostWindow; //是否是置顶窗口 +bool FocusWhenFirstShown; //首次预览时获得焦点 +bool AdjustInitialSizeAndPositionForDPIScale; //根据DPI调整窗口初始坐标和尺寸 +bool UseOSWindowBorder; //使用操作系统自身的窗口边框 +bool HasCloseButton; //是否带有关闭按钮 +bool SupportsMaximize; //是否支持最大化 +bool SupportsMinimize; //是否支持最小化 +bool ShouldPreserveAspectRatio; //是否锁定宽高比 +bool CreateTitleBar; //是否创建标题栏 +bool SaneWindowPlacement; //是否将窗口约束到屏幕内 +bool bDragAnywhere; //是否可拖拽到任意位置 +bool bManualManageDPI; //是否手动调整DPI +``` + +常用的函数有: + +``` c++ +void MoveWindowTo( FVector2D NewPosition ); +void ReshapeWindow( FVector2D NewPosition, FVector2D NewSize ); +void ReshapeWindow( const FSlateRect& InNewShape ); +void Resize( FVector2D NewClientSize ); +void MorphToPosition( const FCurveSequence& Sequence, const float TargetOpacity, const FVector2D& TargetPosition ); +void MorphToShape( const FCurveSequence& Sequence, const float TargetOpacity, const FSlateRect& TargetShape ); + +void ShowWindow(); +void HideWindow(); +void BringToFront( bool bForce = false ); +void RequestDestroyWindow(); +void EnableWindow( bool bEnable ); +void Maximize(); +void Restore(); +void Minimize(); +``` + +### 开发流程 + +Slate开发的绝大部分工作内容可以归纳为: + +- **设置控件属性** +- **绑定事件逻辑** +- **组织层次结构** + +SWidget 是一个抽象基类,UE根据不同的使用方式,又对其进行派生,划分为四大类: + +- **SCompoundWidget** :可以设置ChildSlot + + > Slate 用来提供给开发者的主要扩展方式,一般继承它是为了利用已有的SWidget来组织一系列的Widget。 + +- **SPanel** :可以看做是SWidget的容器,可以包含一个或多个ChildSlot,用于添加SWidget。 + + > Slate已经派生了足够多的SPanel,用于组织Slate的布局等各类操作,一般情况下很少对其派生。 + +- **SLeafWidget** :叶子控件,不包含ChildSlot + + > 派生它,主要是为了自定义一些拥有独特 渲染和尺寸处理机制 的控件 + +- **SWeakWidget** :定义逻辑上的归属而非事件上的。 + + > SPanel中的SWidget在事件上具有层级关系,但有时会出现一些特殊情况,比如点击按钮打开一个菜单,菜单可以看做是隶属于这个按钮的,但本质上菜单是新开启了一个窗口,它们在事件传递上并不存在层级关系,所以就得靠SWeakWidget来解决这个问题。一般情况下很少使用它。 + +大多时候,我们会新增一个C++类,继承自 **SCompoundWidget** ,在`Construct`函数中填充子控件,就像是这样: + +``` c++ +class SCustomWidget: public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SCustomWidget) {} //定义Slate参数 + SLATE_END_ARGS() +public: + void Construct(const FArguments& InArgs){ //使用SNew本质上是调用该函数 + ChildSlot //填充子控件 + [ + SNew(STextBlock) + .Text(FText::FromString("This is body")) + ]; + } +}; +``` + +这样我们就可以使用如下代码来创建该控件: + +``` c++ +auto MyWidget = SNew(SCustomWidget); +``` + +如果想增加 **参数(Argument)** ,比如说让文本内容可以设置,那么可以把定义改为: + +``` c++ +class SCustomWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SCustomWidget) + : _Text(FText::FromString("Default")) { //初始化参数默认值,变量名为参数定义时的变量名前加下划线 + } + SLATE_ARGUMENT(FText,Text) //使用宏SLATE_ARGUMENT定义参数 + SLATE_END_ARGS() +public: + void Construct(const FArguments& InArgs) { + Text = InArgs._Text; //接收传递进来的参数 + ChildSlot + [ + SNew(STextBlock) + .Text(Text) //传递文本内容 + ]; + } +private: + FText Text; +}; +``` + +这样就可以使用如下代码设置控件内容: + +``` c++ +auto MyWidget = SNew(SCustomWidget) + .Text(FText::FromString("Hello")); +``` + +当然,我们也可以不走FArguments的方式,直接这样: + +```c++ +class SCustomWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SCustomWidget){} + SLATE_END_ARGS() +public: + void Construct(const FArguments& InArgs, FText InText) { //直接从Construct的函数参数中传入 + Text = InText; + ChildSlot + [ + SNew(STextBlock) + .Text(Text) + ]; + } +private: + FText Text; +}; + +``` + +``` c +auto MyWidget = SNew(SCustomWidget,FText::FromString("Hello")); //从SNew的参数列表中传入Text +``` + +如果想让上述参数能够绑定委托,就需要使用Slate的 **属性(Attribute)** 机制: + +``` c++ +class SCustomWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SCustomWidget) + : _Text(FText::FromString("Default")) { + } + SLATE_ATTRIBUTE(FText,Text) //使用宏SLATE_ATTRIBUTE定义参数 + SLATE_END_ARGS() +public: + void Construct(const FArguments& InArgs) { + Text = InArgs._Text; //接收传递进来的参数 + ChildSlot + [ + SNew(STextBlock) + .Text(Text) //传递文本属性 + ]; + } +private: + TAttribute Text; //使用TAttribute包裹属性 +}; +``` + +然后就能使用这样的代码: + +```C++ +auto MyWidget = SNew(SCustomWidget) + .Text_Lambda([](){ + return FText::FromString("Hello"); + }); +``` + +如果想增加一些控件的事件处理回调,比如说当上面文本变动时的做一些处理,那么可以用 Slate 的 **事件(Event)** 机制: + +``` c++ +class SCustomWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SCustomWidget) + : _Text(FText::FromString("Default")) { + } + SLATE_ATTRIBUTE(FText,Text) + SLATE_EVENT(FSimpleDelegate, OnTextChanged) //使用宏SLATE_EVENT声明事件 + SLATE_END_ARGS() +public: + void Construct(const FArguments& InArgs) { + Text = InArgs._Text; + OnTextChanged = InArgs._OnTextChanged; //传递事件委托 + ChildSlot + [ + SNew(STextBlock) + .Text(Text) + ]; + } + void SetText(FText InText) { + Text = InText; + OnTextChanged.ExecuteIfBound(); //执行委托 + } + FText GetText(){ + return Text.Get(); + } +private: + FSimpleDelegate OnTextChanged; //定义委托 + TAttribute Text; +}; +``` + +``` c++ +auto MyWidget = SNew(SCustomWidget) + .Text_Lambda([](){ + return FText::FromString("Hello"); + }) + .OnTextChanged_Lambda([](){ //当文字变动时打印日志 + UE_LOG(LogTemp,Warning,TEXT("Oh, Text is changed!")); + }); +``` + +此外,`SLATE_BEGIN_ARGS(...)`和`SLATE_END_ARGS()`之间还有其他可供使用的宏,使用频率不高,因此这里不展开描述,具体可以查看: + +- `Engine\Source\Runtime\SlateCore\Public\Widgets\DeclarativeSyntaxSupport.h` + +综上,Slate的开发流程基本如此。 + +### 控件一览 + +这里主要是为了建立 **控件<--->类型** 的映射,知道有什么控件,当使用这些控件的时候,UE源码中大量的用例可供参考,此外UE还提供相应的调试工具。 + +#### 基础 + +- `SButton`:按钮 + + +![image-20220518154617249](Resources/image-20220518154617249.png) + +- `SCheckBox`:复选框 + + +![image-20220518154711351](Resources/image-20220518154711351.png) + +- `SComboBox`:自定义元素的组合框,需要提供菜单项的控件生成器 + + +![image-20220518154846902](Resources/image-20220518154846902.png) + +- `STextComboBox` :文本组合框 + + +![image-20220518172336987](Resources/image-20220518172336987.png) + +- `SComboButton`:组合按钮,与ComboBox不同的是,ComboButton需要提供一个完整菜单的生成器,而非菜单项生成器 + +![image-20220518155900834](Resources/image-20220518155900834.png) + +- `SInputKeySelector`:按键输入选择器 + + +![image-20220518162532859](Resources/image-20220518162532859.png) + +- `SNumericDropDown`:数字下拉控件 + + +![image-20220518163912751](Resources/image-20220518163912751.png) + +- `SNumericEntryBox`:数字选择框 + + +![image-20220518163925567](Resources/image-20220518163925567.png) + +- `SRotatorInputBox = SNumericRotatorInputBox`:旋转输入框 + + +![image-20220518164200510](Resources/image-20220518164200510.png) + +- `SSlider`:滑动条 + + +![image-20220518171119875](Resources/image-20220518171119875.png) + +- `SSpinBox`:自定义数字类型的微调框 + + +![image-20220518171137321](Resources/image-20220518171137321.png) + +- `SVectorInputBox = SNumericVectorInputBox, 3>`:向量输入框 + + +![image-20220518171848647](Resources/image-20220518171848647.png) + +- `SVolumeControl`:音量调节控件 + + +![image-20220518171209154](Resources/image-20220518171209154.png) + +##### 文本 + +- `STextBlock`:文本显示块 + + +![image-20220519151610023](Resources/image-20220519151610023.png) + +- `SRichTextBlock`:富文本显示块 + +- `SEditableText`:可编辑文本 + + +![image-20220518160850714](Resources/image-20220518160850714.png) + +- `SInlineEditableTextBlock`:单行文本编辑块(在编辑时才会显示边框) + + +![image-20220519151600924](Resources/image-20220519151600924.png) + +- `SEditableTextBox`:可编辑器文本框 + + +![image-20220518161209322](Resources/image-20220518161209322.png) + +- `SSuggestionTextBox`:带智能提示及输入历史的文本框 + + +![image-20220518165125687](Resources/image-20220518165125687.png) + +- `SSearchBox`:搜索框 + + +![image-20220518164213105](Resources/image-20220518164213105.png) + +- `SMultiLineEditableText`:多行文本编辑块 + + +![image-20220519151549183](Resources/image-20220519151549183.png) + +- `SMultiLineEditableTextBox`:多行可编辑文本框 + + +![image-20220518163837277](Resources/image-20220518163837277.png) + +- `SHyperlink`:超链接 + + +![image-20220518161727667](Resources/image-20220518161727667.png) + +##### 颜色 + +- `SColorBlock`:单个颜色的显示控件 + + +![image-20220518175311526](Resources/image-20220518175311526.png) + +- `SSimpleGradient`:简单渐变色的显示控件,只支持设置起始颜色和结束颜色 + + +![](Resources/image-20220518175922270.png) + +- `SComplexGradient`:复杂渐变色的显示控件,支持任意颜色数量组成的渐变色 + + +![image-20220518180009013](Resources/image-20220518180009013.png) + +- `SColorPicker`:单个颜色调节的整合控件(需引入模块 **AppFramework** ) + + +![image-20220518181105385](Resources/image-20220518181105385.png) + +##### 图像 + +- `SImage`:用于显示图像 + + +![image-20220519100214978](Resources/image-20220519100214978.png) + +##### 导航 + +- `SBreadcrumbTrail` + + +![image-20220519101516637](Resources/image-20220519101516637.png) + +##### 通知 + +- `SErrorHint`:错误图标,可设置ToolTip + + +![image-20220519102729140](Resources/image-20220519102729140.png) + +- `SErrorText`:红色背景的文本块 + + +![image-20220519102738687](Resources/image-20220519102738687.png) + +- `FNotificationInfo`:非控件,用于消息通知 + + +![image-20220519102706748](Resources/image-20220519102706748.png) + +```c++ +FNotificationInfo NotificationInfo(FText::FromString("FNotificationInfo")); +NotificationInfo.Text = FText::FromString("This Is Notification Info"); +NotificationInfo.bFireAndForget = true; +NotificationInfo.ExpireDuration = 100.0f; +NotificationInfo.bUseThrobber = true; +FSlateNotificationManager::Get().AddNotification(NotificationInfo); +``` + +- `SProgressBar`:进度条 + + +![image-20220519101724243](Resources/image-20220519101724243.png) + +- `SHeader`:标题控件 + + +![image-20220519142042405](Resources/image-20220519142042405.png) + + - `SScrollBar`:滚动条 + + +![image-20220519143516261](Resources/image-20220519143516261.png) + + - `SSpacer`:空白控件,一般用于在布局中进行填充 + +#### 控件容器(SPanel) + +##### 布局与尺寸 + +- `SVerticalBox`:竖直布局 + +- `SHorizontalBox`:水平布局 + +- `SOverlay`:重叠布局 + +- `SGridPanel`:网格布局 + + +![image-20220519141108149](Resources/image-20220519141108149.png) + +- `SUniformGridPanel`:使用统一网格大小的网格布局面板。 + +- `SWrapBox`:流式布局,可设置排列方向,当控件超出尺寸时,换行。 + + +![image-20220519134544114](Resources/image-20220519134544114.png) + +- `SUniformWrapPanel`:使用统一大小的流式布局 + +- `SCanvas`:可以使用任意坐标和大小来放置Child + +- `SConstraintCanvas` :静态 + +- `SSplitter`:可以拖拽分割线来调整Widget间分割的大小 + + +![image-20220519144844487](Resources/image-20220519144844487.png) + +- `SScrollBox`:为Content添加滚动条 + +![image-20220519133723353](Resources/image-20220519133723353.png) + +- `SWidgetSwitcher`:Widget切换控件,将Widget全部添加到Slot中,通过设置Slot Index来切换显示对应的Widget + + +![image-20220519144306639](Resources/image-20220519144306639.png) + +- `SDPIScaler`:用于调整DPI缩放 + +- `SBox`:控制Content的固定尺寸、最小尺寸或者最大尺寸 + + +![image-20220519133531893](Resources/image-20220519133531893.png) + +- `SScaleBox`:统一缩放Content的尺寸 + + +![image-20220519133607430](Resources/image-20220519133607430.png) + +##### 视图 + +- `SListView`:列表视图 + + +![image-20220519104631179](Resources/image-20220519104631179.png) + +- `STileView`:块视图 + + +![image-20220519105945628](Resources/image-20220519105945628.png) + +- `STreeView`:树视图 + + +![image-20220519110629347](Resources/image-20220519110629347.png) + +##### 停靠窗口 + +- `SDockTab` + + +![image-20220519113722921](Resources/image-20220519113722921.png) + +```C++ +const TSharedRef MajorTab = SNew(SDockTab).TabRole(ETabRole::MajorTab); +TabManager = FGlobalTabmanager::Get()->NewTabManager(MajorTab); + +TabManager->RegisterTabSpawner(FName("MyDockTab"), FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& Args) { + TSharedRef Tab = SNew(SDockTab).Content()[SNew(STextBlock).Text(FText::FromString("MyDockContent"))]; + return Tab; +})).SetDisplayName(FText::FromString("MyDock")) + .SetMenuType(ETabSpawnerMenuType::Hidden); +TabManager->TryInvokeTab(FName("MyDockTab")); +``` + +##### 其他 + +- `SBackgroundBlur`:模糊Content的背景 + + +![image-20220519114840338](Resources/image-20220519114840338.png) + +- `SBorder`:为Content添加边框 + + +![image-20220519133519815](Resources/image-20220519133519815.png) + +- `STooltipPresenter`:为Content添加ToolTip + +- `SExpandableArea`:允许展开或收纳Content + + +![image-20220519142057130](Resources/image-20220519142057130.png) + +- `SFxWidget` +- `SMenuAnchor`:为区域增加上下文菜单 + +### 代码参考 + +UE提供了 **控件反射器** ,可以快速定位到对应UI的创建代码,它的启动点位于编辑器主面板的`顶部菜单栏` - `工具` - `调试` - `控件反射器`: + +![image-20230531094205665](Resources/image-20230531094205665.png) + +另外,官方的引擎源码中也提供了一个 **SlateViewer** 工程展示了大部分Slate控件的基本使用: + +![image-20230531094416657](Resources/image-20230531094416657.png) + +![image-20230531094851667](Resources/image-20230531094851667.png) + +借助IDE的 **代码搜索工具** 可以在引擎中找到对应控件的参数示例,在`Visual Studio`中也可以在`解决方案资源管理器`中查看`类的派生关系`概览所有控件: + +![image-20230531134824751](Resources/image-20230531134824751.png) + +## 高级定制 + +上述技能能满足UI开发的绝大部分需求,但需要涉及到一些复杂的交互机制和渲染逻辑时,就需要自定义SLeafWidget + +这里有一个很好的示例, 实现了一个可拖拽移动的控件: + +![13](Resources/13.gif) + +代码如下: + +``` c++ +#pragma once +#include "Styling/StyleColors.h" +#include "Brushes/SlateColorBrush.h" + +class SCustomSlateWidget :public SLeafWidget { + SLATE_DECLARE_WIDGET(SCustomSlateWidget, SLeafWidget) //使用宏SLATE_DECLARE_WIDGET声明Slate控件 +public: + SLATE_BEGIN_ARGS(SCustomSlateWidget) {} + SLATE_END_ARGS() + void Construct(const SCustomSlateWidget::FArguments InArgs) { + SetCursor(EMouseCursor::GrabHand); //设置鼠标进入控件区域的样式 + } +protected: + FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { + if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton)) { + LastMouseDownPosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());//将鼠标位置从屏幕空间转换到控件的本地空间 + return FReply::Handled().CaptureMouse(SharedThis(this)); //开始高频率捕获全局的鼠标移动 + } + return FReply::Handled(); + } + FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override{ + if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton)) { + auto Window = FSlateApplication::Get().FindWidgetWindow(SharedThis(this)); //获取该控件的窗口 + auto CurrentMousePosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); + auto CurrWindowPosition = Window->GetPositionInScreen(); + auto TargetPosition = CurrentMousePosition + CurrWindowPosition - LastMouseDownPosition; //计算位置移动 + Window->MoveWindowTo(TargetPosition); + } + return FReply::Handled(); + } + FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { + LastMouseDownPosition = FVector2D::ZeroVector; + return FReply::Handled().ReleaseMouseCapture(); // 释放鼠标捕获 + } + int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override { + static FSlateFontInfo FontInfo = FCoreStyle::GetDefaultFontStyle("Bold",30); + static const FSlateBrush* Bursh = FAppStyle::GetBrush("WhiteBrush"); + FLinearColor BackgroundColor = IsHovered()? FLinearColor(0.6f, 0.5f, 0.9f) : FLinearColor(0.1f, 0.5f, 0.9f); + FSlateDrawElement::MakeBox(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Bursh, ESlateDrawEffect::None, BackgroundColor); //绘制背景 + FSlateDrawElement::MakeText(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), FText::FromString("Hello Slate"), FontInfo); //绘制文字 + return LayerId; + } + FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override { + return FVector2D::ZeroVector; // 使用零向量会填充父控件的内容 + } +private: + FVector2D LastMouseDownPosition = FVector2D::ZeroVector; +}; + +// 下方代码需要放置在cpp中 +SLATE_IMPLEMENT_WIDGET(SCustomSlateWidget) // 使用宏SLATE_IMPLEMENT_WIDGET生成代码实现 +void SCustomSlateWidget::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer) //必须实现此函数 +{ +} +``` + +``` c++ +FSlateApplication::Get().AddWindow( + SNew(SWindow) + .ClientSize(FVector2D(200,200)) //设置窗口尺寸为200*200 + .SizingRule(ESizingRule::FixedSize) //固定窗口尺寸 + .SupportsMaximize(false) //关闭最大化 + .SupportsMinimize(false) //关闭最小化 + .CreateTitleBar(false) //不创建标题栏 + .AutoCenter(EAutoCenter::PreferredWorkArea) //居中显示 + .IsTopmostWindow(true) //置顶 + [ + SNew(SCustomSlateWidget) + ] +); +``` + +自定义 **SLeafWidget** 的工作流程是: + +- 使用宏 **SLATE_DECLARE_WIDGET(WidgetType, ParentType)** 声明控件 +- 使用宏 **SLATE_IMPLEMENT_WIDGET(WidgetType)** 在cpp中定义代码实现,并实现函数: + - `void WidgetType::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer)` +- 覆写控件尺寸计算的函数: + - `virtual FVector2D SWidget::ComputeDesiredSize(float LayoutScaleMultiplier) const = 0` +- 覆写各类交互事件。 +- 调整控件的属性状态机 +- 依据属性状态机,在`绘制函数(OnPaint)`中添加绘制元素 + +### 交互事件 + +SWidget提供了非常多的交互事件(以On开头的虚函数)供开发者覆写: + +- `OnFocusReceived`:获得焦点 +- `OnFocusLost`:失去焦点 +- `OnFocusChanging`:焦点发生变化 +- `OnKeyChar`:当键盘输入时 +- `OnKeyDown`:键盘按键按下 +- `OnKeyUp`:键盘按键抬起 +- `OnMouseButtonDown`:鼠标按键按下 +- `OnMouseButtonUp`:鼠标按键抬起 +- `OnMouseMove`:鼠标移动 +- `OnMouseEnter`:鼠标进入到Widget区域的瞬间 +- `OnMouseLeave`:鼠标离开到Widget区域的瞬间 +- `OnMouseButtonDoubleClick`:鼠标双击 +- `OnMouseWheel`:鼠标滚轮滚动 +- `OnDragEnter`:拖拽东西进入Widget区域的瞬间 +- `OnDragLeave`:拖拽东西离开Widget区域的瞬间 +- `OnDragOver`:拖拽东西覆盖Widget区域的时候 +- `OnDrop`:在Widget上释放拖拽的东西 +- ... + +> 详细说明,请看SWidget的定义,上面有各个事件的说明及触发时机,关于用法可在UE源码中搜索相应的用例。 + +使用说明: + +- 这些事件由 **FSlateApplication** 负责调用,其函数参数一般包含了该事件携带的所有信息,我们只需对其进行覆盖(override),根据事件数据,做自己的逻辑即可。 + +- 事件的返回类型为 **FReply** ,用于定义事件的后续处理,它的构造函数是私有的,必须使用如下静态函数去创建实例: + + - `FReplay::Handled()`:说明该事件已被处理,事件将不会继续往下一级传递。 + - `FReplay::Unhandled()`:说明该控件并不处理该事件,事件会继续往下一级传递。 + + **FReply** 提供了一系列操作用于在事件结束时做一些处理: + +![image-20230531164053994](Resources/image-20230531164053994.png) + +### 绘制事件 + +SWidget的实际绘制函数为`Paint`,但由于绘制过程中存在一些固定步骤,因此Slate公开了绘制事件`OnPaint`供我们自定义绘制逻辑,它的定义如下: + +``` c++ +virtual int32 OnPaint(const FPaintArgs& Args, + const FGeometry& AllottedGeometry, + const FSlateRect& MyCullingRect, + FSlateWindowElementList& OutDrawElements, + int32 LayerId, + const FWidgetStyle& InWidgetStyle, + bool bParentEnabled) const = 0; +``` + +参数说明: + +- `const FPaintArgs&` Args:绘制事件的参数,可以得到绘制的当前时间、间隔、父窗口... +- `const FGeometry& ` AllottedGeometry:分配给该SWidget的几何大小(相对于父窗口) +- ` const FSlateRect&` MyCullingRect:该SWidget的裁剪举行,可以在绘制中根据该矩形丢弃绘制元素 +- `FSlateWindowElementList&` OutDrawElements:这是整个SWindow的绘制元素列表,我们绘制则是对该对象进行修改。 +- `int32` LayerId:绘制的LayerID,一般用于给UI分层,方便对同层级的绘制元素做统一处理。 +- `const FWidgetStyle&` InWidgetStyle:父窗口传递过来的Style +- `bool` bParentEnabled:父窗口是否处于开启状态。 + +在`OnPaint`函数中,我们主要通过 **FSlateDrawElement** 中很多命名以`Make`开头的静态函数来创建绘制元素,其中有: + +``` c++ +static void MakeBox( //绘制盒子,通过设置Brush,可以是颜色块,也可以是图片 + FSlateWindowElementList& ElementList, + uint32 InLayer, + const FPaintGeometry& PaintGeometry, + const FSlateBrush* InBrush, + ESlateDrawEffect InDrawEffects = ESlateDrawEffect::None, + const FLinearColor& InTint = FLinearColor::White ); + +static void MakeRotatedBox(...); //绘制一个具有旋转的盒子 +static void MakeGradient(...); //绘制渐变的盒子 +static void MakeText(...); //绘制文本 +static void MakeShapedText(...); //绘制形状文本 +static void MakeLines(...); //绘制线条 +static void MakeSpline(...) //绘制曲线 +static void MakeDrawSpaceSpline(...); //在绘制空间绘制曲线 +static void MakeCubicBezierSpline(...); //绘制三阶贝塞尔曲线 +static void MakeViewport(...) //绘制FSlateViewport +static void MakeCustom(...); //绘制自定义元素,可自行实现元素的Render函数 +static void MakeCustomVerts(...); //根据顶点进行绘制 +static void MakePostProcessPass(...); //绘制后期滤镜(模糊) +``` + +这些函数在UE中有非常多的使用,可以搜索相关代码来参考。 + +> 详见:`Runtime\SlateCore\Public\Rendering\DrawElements.h` + +#### FSlateBrush + +**FSlateBrush** 用于控制填充区域,在界面中有较高的使用频率,因此Slate也提供了一些它的便捷扩展: + +![image-20230531165625590](Resources/image-20230531165625590.png) + +- **FSlateBorderBrush** :填充边框 +- **FSlateBoxBrush** :盒子画刷 +- **FSlateRoundedBoxBrush** :圆角矩形画刷 +- **FSlateColorBrush** :颜色画刷 +- **FSlateImageBrush** :图片 + - **FSlateVectorImageBrush** :矢量图 +- **FSlateDynamicImageBrush** :可动态切换图片的画刷 +- **FSlateNoResource** :不带任何资源的画刷 + +> 上面的一些画刷本质上都是通过一些特定的参数去构造 **FSlateBrush** ,如果出现上述画刷需要组合的情况,可根据它们的构造参数自行组合。 + +UE中提供了一些内置的 **FSlateBrush** ,可以在源码中搜索`::Get().GetBrush`来查找相关使用: + +![image-20230531180012391](Resources/image-20230531180012391.png) + +##### 示例 + +假如想让整个SWidget填充红色,可以使用这样的代码: + +```c++ +int32 SCustomSlateWidget::OnPaint(const FPaintArgs& Args, + const FGeometry& AllottedGeometry, + const FSlateRect& MyCullingRect, + FSlateWindowElementList& OutDrawElements, + int32 LayerId, + const FWidgetStyle& InWidgetStyle, + bool bParentEnabled) const { + static FSlateColorBrush ColorBrush(FColor(255,0,0)); + FSlateDrawElement::MakeBox(OutDrawElements, + LayerId, + AllottedGeometry.ToPaintGeometry(), + &ColorBrush, + ESlateDrawEffect::None, + ColorBrush.TintColor.GetColor(InWidgetStyle)); + return LayerId; +} +``` + +#### ESlateDrawEffect + +**ESlateDrawEffect** 为Slate提供了一些效果,它可以是以下值: + +``` c++ +enum class ESlateDrawEffect : uint8 +{ + None = 0, //无效果 + NoBlending = 1 << 0, //无混合 + PreMultipliedAlpha = 1 << 1, //预乘Alpha + NoGamma = 1 << 2, //不进行Gamma矫正 + InvertAlpha = 1 << 3, //反转透明度 + // ^^ These Match ESlateBatchDrawFlag ^^ + + NoPixelSnapping = 1 << 4, //关闭像素对齐 + DisabledEffect = 1 << 5, //禁用状态的效果 + IgnoreTextureAlpha = 1 << 6, //忽略纹理中的透明度 + + ReverseGamma = 1 << 7 //逆转Gamma +}; +``` + +#### FSlateFontInfo + +**FSlateFontInfo** 用于指定字体信息,对应UE里的字体资产, 它的部分定义如下: + +``` c++ +struct SLATECORE_API FSlateFontInfo +{ + /**The font object (valid when used from UMG or a Slate widget style asset) */ + TObjectPtr FontObject; + + /**The material to use when rendering this font */ + TObjectPtr FontMaterial; + + /**Settings for applying an outline to a font */ + FFontOutlineSettings OutlineSettings; + + /**The composite font data to use (valid when used with a Slate style set in C++) */ + TSharedPtr CompositeFont; + + /**The name of the font to use from the default typeface (None will use the first entry) */ + FName TypefaceFontName; + + /** + * The font size is a measure in point values. The conversion of points to Slate Units is done at 96 dpi. So if + * you're using a tool like Photoshop to prototype layouts and UI mock ups, be sure to change the default dpi + * measurements from 72 dpi to 96 dpi. + */ + int32 Size; + + /**The uniform spacing (or tracking) between all characters in the text. */ + int32 LetterSpacing = 0; + + /**The font fallback level. Runtime only, don't set on shared FSlateFontInfo, as it may change the font elsewhere (make a copy). */ + EFontFallback FontFallback; +}; +``` + +UE中有专门的字体编辑器,在C++中直接构造它比较麻烦,不过UE也提供了一些内置的 **FSlateFontInfo** ,比如: + +- `FCoreStyle::Get().GetFontStyle("NormalFont")` +- `FCoreStyle::Get().GetFontStyle("SmallFont")` +- `FCoreStyle::GetDefaultFontStyle("Regular",FontSize);` +- `FCoreStyle::GetDefaultFontStyle("Bold",FontSize)` +- `FCoreStyle::GetDefaultFontStyle("Italic",FontSize);` +- `FCoreStyle::GetDefaultFontStyle("Mono",FontSize);` + +##### FontMeasure + +大多数GUI框架都有 **字体测量(FontMeasure)** 的概念:测量特定字体格式的某个文本的绘制宽度和高度。 + +在UE中,可以通过如下方式来测量(伪代码): + +``` c++ +FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->Measure( + const FText& Text, //文本 + const FSlateFontInfo& InFontInfo, //字体 + float FontScale //字体缩放 +) -> FVector2D //返回宽度(X)和高度(Y) +``` + +### 性能优化 + +Slate为了优化UI的`Paint`和`Tick`性能,提供了一个 **SInvalidationPanel** 控件,该控件可以让子控件中消耗较高的操作进行缓存,只有当一些特定操作导致 **缓存失效(Invalidate)** ,才会重新执行。 + +详见:[理解虚幻引擎Slate 架构 - 轮询数据流和委托 | 虚幻引擎5.2文档 ](https://docs.unrealengine.com/5.2/zh-CN/understanding-the-slate-ui-architecture-in-unreal-engine/#轮询数据流和委托) + +我们可以手动调用子控件的`Invalidate()`函数来让缓存失效: + +``` c++ +void SWidget::Invalidate(EInvalidateWidgetReason InvalidateReason); +``` + +**EInvalidateWidgetReason** 的值可以是: + +``` c++ +enum class EInvalidateWidgetReason : uint8 +{ + None = 0, + Layout = 1 << 0, //如果需要更改Widget所需的大小,请使布局无效。这是一个昂贵的无效操作,所以如果你只需要重新绘制一个小部件,就不要使用它 + Paint = 1 << 1, //使绘制无效 + Volatility = 1 << 2, //当Volatility变动时触发 + ChildOrder = 1 << 3, //当添加或移除自控件时触发,使之无效会重建Child的顺序 + RenderTransform = 1 << 4, //当RenderTransform变动时触发 + Visibility = 1 << 5, //当可见性变动时触发 + AttributeRegistration = 1 << 6, //绑定属性时触发 + Prepass = 1 << 7, //使Prepass无效 + + /** + * Use Paint invalidation if you're changing a normal property involving painting or sizing. + * Additionally if the property that was changed affects Volatility in anyway, it's important + * that you invalidate volatility so that it can be recalculated and cached. + */ + PaintAndVolatility = Paint | Volatility, + /** + * Use Layout invalidation if you're changing a normal property involving painting or sizing. + * Additionally if the property that was changed affects Volatility in anyway, it's important + * that you invalidate volatility so that it can be recalculated and cached. + */ + LayoutAndVolatility = Layout | Volatility, +}; +``` + +Slate提供了一种基于 **属性(SlateAttribue)** 的方式:让属性的变动跟缓存的重建挂钩 + +具体的做法是: + +- 使用 **TSlateAttribute** 包裹属性(跟 **TAttribute** 的用途不同), **TSlateAttribute** 包裹的属性在构造初始化时需要传递参数`(*this)` + +- 在`void WidgetType::PrivateRegisterAttributes(...)`函数中,使用宏 **SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION(_Initializer, _Property, _Reason)** 来注册属性 + +这里有一个简短的示例: + +``` c++ +class SCustomSlateWidget :public SLeafWidget { + SLATE_DECLARE_WIDGET(SCustomSlateWidget, SLeafWidget) +public: + SLATE_BEGIN_ARGS(SCustomSlateWidget){} + SLATE_END_ARGS() + void Construct(const SCustomSlateWidget::FArguments InArgs){ + } +protected: + SCustomSlateWidget() + : ClickedCounter(*this) { //初始化SlateAttribute + } + FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override { + ClickedCounter.Set(*this, ClickedCounter.Get() + 1); //计数器+1,这里把鼠标按下当做是点击 + return FReply::Handled(); + } + int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override { + static FSlateFontInfo FontInfo = FCoreStyle::GetDefaultFontStyle("Bold",30); + FSlateDrawElement::MakeText(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), FText::FromString(FString::FromInt(ClickedCounter.Get())), FontInfo); //绘制当前鼠标点击的次数 + return LayerId; + } + FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override { + return FVector2D::ZeroVector; + } +private: + TSlateAttribute ClickedCounter; //记录鼠标点击次数的计数器 +}; +``` + +``` c++ +SLATE_IMPLEMENT_WIDGET(SCustomSlateWidget) +void SCustomSlateWidget::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer) { + //将ClickedCounter的值变动跟重绘事件挂钩 + //SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION(AttributeInitializer, ClickedCounter, EInvalidateWidgetReason::Paint); +} +``` + +``` c++ +FSlateApplication::Get().AddWindow( + SNew(SWindow) + .SupportsMaximize(false) + .SupportsMinimize(false) + .IsTopmostWindow(true) + .ClientSize(FVector2D(200,200)) + .SizingRule(ESizingRule::FixedSize) + .CreateTitleBar(false) + .AutoCenter(EAutoCenter::PreferredWorkArea) + [ + SNew(SInvalidationPanel) // 使用SInvalidationPanel包括 + [ + SNew(SCustomSlateWidget) + ] + ] +); +``` + +该代码创建了一个可点击的方块: + +![image-20230531182616000](Resources/image-20230531182616000.png) + +点击方块会让`ClickedCounter+1`,但画面不会发生任何变化,当取消`PrivateRegisterAttributes`中注释的代码: + +``` c++ +void SCustomSlateWidget::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer) { + //将ClickedCounter的值变动跟重绘事件挂钩 + SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION(AttributeInitializer, ClickedCounter, EInvalidateWidgetReason::Paint); +} +``` + +重新启动程序,可以发现,当`ClickedCounter`变更时,UI会重新绘制: + +![53](Resources/53.gif) + +## 从Slate到UMG + +UE的逻辑编程建立在UObject之上,而Slate为了追求极致的性能,并没有使用UObject这种沉重的机制去管理UI对象 + +在Slate中,使用的是 **共享指针(Shared Pointer)** 的方式来管理UI对象的生命周期 + +为了提升游戏开发者的UI开发效率,UE使用UObject对Slate控件包裹了一层,通过反射暴露了Slate控件的一些属性和函数,同时支持了可视化的UI设计,这一层接口也就是我们熟知的 **UMG** + +关于 **UMG** ,官方文档中有详细的介绍: + +- https://docs.unrealengine.com/5.2/zh-CN/umg-ui-designer-for-unreal-engine/ + +这里有一个简短的代码很好地体现了Slate和UMG的关系: + +``` c++ +#pragma once + +#include "UObject/ObjectMacros.h" +#include "Widgets/Text/STextBlock.h" +#include "Components/Widget.h" +#include "CustomUWidget.generated.h" + +UCLASS(meta = (DisplayName = "CustomWidget")) //DisplayName是控件名称 +class UMGEXTENDUTILITIES_API UCustomWidget : public UWidget +{ + GENERATED_BODY() +protected: + virtual TSharedRef RebuildWidget() override { //重建控件,并赋值给成员变量 + return SAssignNew(MyTextBlock,STextBlock); + } + virtual void SynchronizeProperties() override { //将UObejct的属性同步到Slate控件中 + MyTextBlock->SetText(Text); + Super::SynchronizeProperties(); + } + virtual void ReleaseSlateResources(bool bReleaseChildren) override { //释放Slate资源 + MyTextBlock.Reset(); + } + +#if WITH_EDITOR + virtual const FText GetPaletteCategory() override { //在编辑器上的控件分类 + return FText::FromString("CustomCategory"); + } +#endif + +public: + UFUNCTION(BlueprintCallable) //公开函数到蓝图 + FText GetText() const { + return Text; + } + UFUNCTION(BlueprintCallable) + void SetText(FText InText) { + Text = InText; + if (MyTextBlock) { //设置属性,当控件已经创建的时候,直接把值塞到控件里面 + MyTextBlock->SetText(Text); + } + } +protected: + UPROPERTY(EditAnywhere, BlueprintGetter = "GetText", BlueprintSetter = "SetText") + FText Text; //公开属性到编辑器 + + TSharedPtr MyTextBlock; //实际的Slate控件 +}; +``` + +![image-20230601182230193](Resources/image-20230601182230193.png) + +在C++中,使用UMG的方法是: + +``` c++ +UWidget* UMGWidget = LoadObject(nullptr, TEXT("{AssetPath}")); //从资产加载,也可以NewObject +TSharedPtr SlateWidget = UMGWidget->TakeWidget(); //该操作会创建一个新的SWidget, +TSharedPtr CachedSlateWidget = UMGWidget->GetCachedWidget(); //获取上一次创建的SWidget +//之后再使用Slate的使用方式 +``` + +### 生命周期管理 + +**UWidget** 走的是 **UObject** 的垃圾回收,它持有了 **SWidget** 的共享指针,为了保证控件在显示时(`TSharedPtr`还存在时), **UWidget** 不会被当成垃圾回收,UMG使用了一个继承自 **FGCObject** 的控件 **SObjectWidget** 对 **UWidget** 增加了引用 + +![image-20230602103618309](Resources/image-20230602103618309.png) + +再回到上面的示例代码中,可以了解到,调用`UWidget::TakeWidget()`得到的并不是 `UWidget::RebuildWidget()`中创建的控件,而是对其又用 **SObjectWidget** 包裹了一层,它们在创建过程中的层次关系如下: + +![图片2](Resources/%E5%9B%BE%E7%89%872.png) + +在销毁时,它们的释放顺序如下: + +![图片3](Resources/%E5%9B%BE%E7%89%873.png) + +- **SObjectWidget** 继承自 **FGCObject** ,它会添加对UWidget的引用,保证 **SObjectWidget** 存在时, **UWidget** 不会被GC +- 当控件开始销毁时, **SObjectWidget** 的引用计数为0会被销毁,此时会解除对 **UWidget** 的引用 +- 当 **UWidget** 的引用为0,则会在下一次垃圾回收时将其释放,此时才会释放其持有的 **SWidget** 共享指针 + diff --git "a/Docs/04-UnrealEngine/2.\346\217\222\344\273\266\345\274\200\345\217\221.md" "b/Docs/04-UnrealEngine/2.\346\217\222\344\273\266\345\274\200\345\217\221.md" new file mode 100644 index 00000000..6981fb0b --- /dev/null +++ "b/Docs/04-UnrealEngine/2.\346\217\222\344\273\266\345\274\200\345\217\221.md" @@ -0,0 +1,715 @@ +--- +comments: true +--- + +插件 游离在 引擎和工程代码 之外,它是UE中用来扩展 程序功能 的主要方式, + +插件本质上是一种模块化的代码设计,遵循这一设计能减少代码间的依赖,使后续更易于迭代和维护。 + +在UE中,一个插件的标准文件结构如下: + +![image-20230525143413268](Resources/image-20230525143413268.png) + +- `Binaries`:存放编译的生成文件 +- `Content`:存放插件的资产内容 +- `Intermediate`:存放编译过程中生成的中间文件 +- `Resources`:存放插件的资源文件,比如图标,非UE资产 +- `Source`:存放代码源文件,可包含多个Module + +- `*.uplugin`:插件的结构组织文件 + +其中 `*.uplugin` 的基本内容如下: + +``` c++ +{ + "FileVersion": 3, //文件夹版本 + "Version": 1, //版本Number + "VersionName": "1.0", //版本名称 + "FriendlyName": "CustomPlugin", //别名 + "Description": "", //描述 + "Category": "Other", //分类 + "CreatedBy": "", //作者 + "CreatedByURL": "", //作者链接 + "DocsURL": "", //文档链接 + "MarketplaceURL": "", //商城链接 + "SupportURL": "", //服务支持链接 + "CanContainContent": false, //是否包含内容目录 + "IsBetaVersion": false, //是否为测试版本 + "IsExperimentalVersion": false, //是否为实验性版本 + "Installed": false, //是否嵌入 + "Modules": [ //模块定义 + { + "Name": "CustomPlugin", //模块名称 + "Type": "Editor", //模块类型 + "LoadingPhase": "Default" //加载时机 + } + //, //其他模块 + //{ + // ... + //} + ] +} +``` + +关于这些参数的配置,详见: + +- https://docs.unrealengine.com/5.2/en-US/unreal-engine-modules + +在UE中开发插件,常见的工作流是: + +- 点击 `编辑` - `插件` - 打开插件面板 + + ![image-20230525145122582](Resources/image-20230525145122582.png) + +- 添加新插件 + + ![image-20230525145306629](Resources/image-20230525145306629.png) + +- 创建空白插件 + + ![image-20230525145357430](Resources/image-20230525145357430.png) + +- 由于项目结构发生变化,此时需要重新生成IDE的工程文件 + +> 空白插件是一个很好的起点,其他只不过是增加了一些附带的代码模板,如果对其并不熟悉,可以尝试添加看看,了解一下里面的代码具体增加了哪些内容,掌握了它的作用,往空白插件里加就行 + +这是一个C++插件中必需存在的文件: + +``` c++ +// MyPlugin.h + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FMyPluginModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; +``` + +``` c++ +// MyPlugin.cpp + +#include "MyPlugin.h" + +#define LOCTEXT_NAMESPACE "FMyPluginModule" + +void FMyPluginModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FMyPluginModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FMyPluginModule, MyPlugin) //定义插件入口 +``` + +UE在加载插件时会调用`StartupModule()`,卸载插件时会调用`ShutdownModule()` + +插件开发的代码流程主要是: + +- 在`StartupModule()`中,调用 **核心模块的函数接口** 或使用 **核心模块的全局单例** 做一些操作,或者说将一些逻辑挂载到 **核心模块的入口点** 上去做一些扩展。 +- 在`ShutdownModule()`中,清理`StartupModule()`中所做的操作 + +其中常见的一些函数模块如下: + +- **FFileHelper** :文件操作 +- **FPlatformProcess** :平台相关的操作 +- **FBlueprintEditorUtils** :蓝图操作 +- **FKismetEditorUtilities** :编辑器操作 +- **FEdGraphUtilities** :图表操作 +- **FComponentEditorUtils** :组件编辑器操作 +- **FEnumEditorUtils** :枚举操作 +- **FStructureEditorUtils** :结构体操作 +- **FEditorFolderUtils** :编辑器文件夹操作 +- **FMaterialEditorUtilities** :材质操作 +- **FPluginUtils** :插件操作 +- **UGameplayStatics** :Gameplay相关操作 +- ... + +全局单例如下: + +- `GEngine`,`GEditor`,`GWorld`:引擎里面常用的全局单例 +- `FModuleManager::LoadModuleChecked("AssetTools").Get()`:资产的创建,删除,重命名,导入导出... +- `FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get()`:资产的搜索,枚举... +- `FModuleManager::LoadModuleChecked("Settings")`:配置文件的相关管理 +- `FModuleManager::LoadModuleChecked("PropertyEditor"):属性编辑器的相关操作` +- `FModuleManager::LoadModuleChecked(TEXT("LevelEditor")):关卡编辑器的相关操作` +- `FModuleManager::LoadModuleChecked("Kismet"):蓝图编辑器的相关操作` +- ... + +核心模块的入口点,一般指的是: + +- **Callback 函数回调** +- **Delegate 委托** + +常见的委托定义集合有: + +- **FCoreUObjectDelegates** :UObject相关的委托 + +- **FCoreDelegates** :核心模块的相关委托 + +- **FEditorDelegates** :编辑器模块的相关委托 + +- **FWorldDelegates** :游戏世界相关的委托 +- **FGameDelegates** +- ... + +在编写完插件,执行编译时,会在插件的`Binaries`目录下生成以下文件: + +![image-20230525162605340](Resources/image-20230525162605340.png) + +- `*.module`:定义模块库的基本信息 +- `*.dll`:动态库 +- `*.pdb`:程序调试数据库,用于通过源码 断点调试 库,详见 [此处](https://learn.microsoft.com/en-us/visualstudio/debugger/specify-symbol-dot-pdb-and-source-files-in-the-visual-studio-debugger) + +其中*.module的文件内容如下 + +``` c++ +{ + "BuildId": "a36b278a-b317-44c3-8743-b82361b74343", + "Modules": + { + "MyPlugin": "UnrealEditor-MyPlugin.dll" + } +} +``` + +这里需要注意的参数是`BuildID`,它是由引擎生成的,当插件的`BuildID`与引擎的`BuildID`不一致时,使用编辑器启动项目会报错: + +![image-20230525165851860](Resources/image-20230525165851860.png) + +引擎的`BuildID`可以查看: + +![image-20230525170207274](Resources/image-20230525170207274.png) + +## 基础 + +## 菜单栏工具栏扩展 + +引擎开发过程中,经常会有一些需求,去扩展引擎编辑器的菜单栏,工具栏,增加一个按钮,去执行一些便捷的操作,就像是这样: + +![image-20220526100322912](Resources/image-20220526100322912.png) + +![image-20220526115630692](Resources/image-20220526115630692.png) + +在UE4中,扩展菜单的主要方式是通过 **FExtender** ,到了UE5中,有了新的更易于使用的方式 — **UToolMenus** + +**UToolMenus ** 是一个单例类,可以通过以下代码获取单例实例: + +``` c++ +UToolMenus* ToolMenus = UToolMenus::Get(); +``` + +在UE5中,所有 可扩展的菜单栏工具栏 `在初始化时` ,都会调用函数 `UToolMenus::RegisterMenu` 进行注册: + +```c++ +UToolMenu* UToolMenus::RegisterMenu(const FName InName, + const FName InParent = NAME_None, + EMultiBoxType InType = EMultiBoxType::Menu, + bool bWarnIfAlreadyRegistered = true) +``` + +![image-20230524155859423](Resources/image-20230524155859423.png) + +该函数用于注册一个可被扩展的`ToolMenu`,函数的各个参数意义如下: + +- `InName`:注册所用的链式字段,形如`LevelEditor.LevelEditorToolBar.User`,之后可以使用该字段作为Key可以得到`UToolMenu` +- `InParent`:指定工具菜单的Parent +- `InType`:指定了该ToolMenu的类型,这个类型就决定了可以怎样来扩展该Menu,它可以是以下的值: + - `MenuBar`:菜单栏,支持扩展菜单 + - `ToolBar`:工具栏,可以添加工具按钮 + - `VerticalToolBar`:竖直工具栏 + - `SlimHorizontalToolBar`:水平工具栏,它是工具栏的精简版本,可以将图标和文本元素水平对齐 + - `UniformToolBar`: + - `Menu`:菜单,可以添加菜单项 + - `ButtonRow`:按行排列的按钮,每行有最大数量的按钮,类似于工具栏,但可以有多行 +- `bWarnIfAlreadyRegistered`:Bool值,如果该字段已被注册则发出警告。 + +当需要 `预览菜单的时刻` ,则会调用函数`UToolMenus::GenerateWidget`来生成`ToolMenu`的控件: + +```c++ +TSharedRef UToolMenus::GenerateWidget(const FName Name, const FToolMenuContext& InMenuContext) +``` + +![image-20230524154628869](Resources/image-20230524154628869.png) + +在`ToolMenu`被Register之后,预览之前的任一时机,我们都可以通过函数`UToolMenus::ExtendMenu`来扩展菜单栏的内容。 + +``` c++ +UToolMenu* UToolMenus::ExtendMenu(const FName InName) +``` + +![image-20230524160554125](Resources/image-20230524160554125.png) + +上方代码中有大量的扩展参考,而这里也有一个简单的插件扩展实例: + +``` c++ +#pragma once + +#include "Modules/ModuleManager.h" + +class FCustomPluginModule : public IModuleInterface +{ +public: + virtual void StartupModule() override { + //启动插件时给UToolMenus的启动增加一个回调 + UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FCustomPluginModule::RegisterMenus)); + } + + virtual void ShutdownModule() override { + //去除UToolMenus的启动回调,重置一些状态 + UToolMenus::UnRegisterStartupCallback(this); + UToolMenus::UnregisterOwner(this); + } +private: + void RegisterMenus() { + FToolMenuOwnerScoped OwnerScoped(this); + UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); + FToolMenuSection& Section = Menu->FindOrAddSection("PluginTools"); + FToolMenuEntry& MenuEntry = Section.AddEntry( + FToolMenuEntry::InitToolBarButton( //添加工具栏按钮 + "CustomToolbarButton", + FUIAction + ( + FExecuteAction::CreateLambda([]() { + FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("CustomNs", "HelloWorld", "Hello World!")); + }) + ) + ) + ); + MenuEntry.InsertPosition = FToolMenuInsert(NAME_None, EToolMenuInsertType::First); + } +}; +``` + +该代码在关卡编辑器主面板的工具栏上添加了一个按钮,在点击时会弹出`Hello World`的对话框: + +![image-20230524163937497](Resources/image-20230524163937497.png) + +我们可以在`UToolMenus::Get()`添加断点,通过VS的调试窗口来获取到所有可扩展的字段列表: + +![image-20230524164244564](Resources/image-20230524164244564.png) + +## 资产扩展 + + + +## 视口扩展 + +扩展视口需要使用以下几个结构 + +- SEditorViewport +- FEditorViewportClient + +## 细节面板相关 + +**DetailView** 是 UE 反射所带来的另一个强大的功能: + +## 蓝图扩展 + +- https://ikrima.dev/ue4guide/editor-extensions/custom-blueprints/blueprint-static-analysis/ + +## 用户体验 + +### 撤销重做 + +UObject拥有一套事务机制,如果UObject拥有标志`RF_Transactional`,则可以使用如下代码: + +```C++ +GEditor->BeginTransaction(NSLOCTEXT("NS", "Transaction", "Transaction")); //开始事务 +Object->Modify(); //此操作将Object的当前状态存储到事务中 + +SomeChange(Object); //修改Object的一些属性 + +GEditor->EndTransaction(); //结束事务,提交Object的新状态 +``` + +上述操作会使得SomeChanged能够被撤销重做。 + +> 详细请参阅:https://blog.csdn.net/qq_29523119/article/details/96778797 + +在编辑器开发中,经常会需要某个对象或窗口在撤销重做时,需要响应变更(比如说进行一些重建),UE提供了接口 **FEditorUndoClient** 用于撤销重做事件的监听,一个简单的代码示例如下: + +```C++ +class FUndoRedoListener: public FEditorUndoClient{ +public: + FUndoRedoListener(){ + if (GEditor) + GEditor->RegisterForUndo(this); //在合适的时机,注册该监听器,不一定是在构造函数中 + } + ~FUndoRedoListener(){ + if (GEditor) + GEditor->UnregisterForUndo(this); //取消注册 + } +protected: + virtual void PostUndo(bool bSuccess) override{ //当编辑器执行撤销后,会调用该函数 + RebuildSomething(); + } + virtual void PostRedo(bool bSuccess) override{ //当编辑器执行重做后,会调用该函数 + RebuildSomething(); + } +}; +``` + +### 反馈交互 + +#### 通知 + +![image-20220927175435715](Resources/image-20220927175435715-16735895166583-168492054713617.png) + +```c++ +FNotificationInfo Info(FText::FromString(TEXT("This is notification"))); +Info.FadeInDuration = 2.0f; +Info.ExpireDuration = 2.0f; +Info.FadeOutDuration = 2.0f; +FSlateNotificationManager::Get().AddNotification(Info); +``` + +![image-20220927141254086](Resources/image-20220927141254086-16735895184905-168492056446620.png) + +``` c++ +static TWeakPtr NotificationPtr; +FNotificationInfo Info(FText::FromString(TEXT("This is notification"))); +Info.FadeInDuration = 2.0f; +Info.FadeOutDuration = 2.0f; +Info.bFireAndForget = false; +Info.bUseThrobber = false; +FNotificationButtonInfo BtYesInfo = FNotificationButtonInfo( + NSLOCTEXT("NotificationNamespace","Yes", "Yes"), + NSLOCTEXT("NotificationNamespace","Yes", "Yes"), + FSimpleDelegate::CreateLambda([]() { + TSharedPtr Notification = NotificationPtr.Pin(); + if (Notification.IsValid()) + { + Notification->SetEnabled(false); + Notification->SetExpireDuration(0.0f); + Notification->ExpireAndFadeout(); + NotificationPtr.Reset(); + } + }), + SNotificationItem::ECompletionState::CS_None +); +Info.ButtonDetails.Add(BtYesInfo); +NotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); +``` + +#### 进度显示 + +![image-20220927210941350](Resources/image-20220927210941350-16735895089921-168492058032923.png) + +``` c++ +float AmountOfWork = 50; +FScopedSlowTask RootTask(AmountOfWork, (NSLOCTEXT("SlowTaskNamespace", "This is slow root task", "This is slow root task"))); +RootTask.MakeDialog(); +for (int i = 0; i < 50; i++) { + FScopedSlowTask SubTask(1, (NSLOCTEXT("SlowTaskNamespace", "This is slow sub task", "This is slow sub task "))); + SubTask.MakeDialog(); + for (int j = 0; j < 2; j++) { + FPlatformProcess::Sleep(0.5); + SubTask.EnterProgressFrame(0.5, FText::FromString("Sub Task")); + } + RootTask.EnterProgressFrame(1, FText::FromString("Root Task")); +} +``` + +#### [对话框](https://zhuanlan.zhihu.com/p/268069477) + +![image-20220928100532108](Resources/image-20220928100532108-167358956103511-168492058697726.png) + +``` c++ +EAppReturnType::Type Ret = FMessageDialog::Open(EAppMsgType::YesNo, NSLOCTEXT("NS", "message dialog", "message dialog")); +if (Ret == EAppReturnType::Yes) { + +} +``` + + + +## 基础操作 + +### 字符串 + +#### TCHAR + +> 通常情况下,代码中的字符串将使用 **ANSI** 进行编码,但由于 **ANSI** 支持的字符数量很少,所以应该在设置字符串变量文字时使用 **TEXT()** 宏,将 **ANSI** 文字转换为 **TCHAR** (本机 Unicode 编码) + +``` c++ +TCHAR* ThisIsTChars = TEXT("This is Raw String"); +``` + +希望转换TCHAR和原生字符串编码格式,可使用以下宏: + +- **TCHAR_TO_ANSI** (str) + +- **TCHAR_TO_UTF8** (str) +- **TCHAR_TO_UTF16** (str) +- **TCHAR_TO_UTF32** (str) +- **TCHAR_TO_WCHAR** (str) +- **ANSI_TO_TCHAR** (str) + +- **UTF8_TO_TCHAR** (str) +- ... + +游戏对于性能的要求是很高的,普通的String满足不了UE游戏开发中各个应用场景的性能需求,所以UE4提供了三个常用字符串类: + +#### [FString](https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/StringHandling/FString/) + +> 通用字符串,可以修改,它提供了大量操作字符串的方法。 + +- 构造 + - static FString **Chr** ( TCHAR Ch ) + - static FString **ChrN** ( int32 NumCharacters, TCHAR Char ) + - static FString **FromInt** ( int32 Num ) + - static FString **SanitizeFloat** ( double InFloat, const int32 InMinFractionalDigits = 1 ) + - static FString **FromBlob** (const uint8* SrcBuffer,const uint32 SrcSize); +- 格式化 + - static FString **Printf** (const FmtType& Fmt, Types... Args) + - FString& **Appendf** (const FmtType& Fmt, Types... Args) + - static FString **Format** (const TCHAR* InFormatString, const FStringFormatOrderedArguments& InOrderedArguments) + - static FString **Format** (const TCHAR* InFormatString, const FStringFormatNamedArguments& InNamedArguments) + +- 转化 + - void **ToUpperInline** () + - FString **ToUpper** () + - void **ToLowerInline** () + - FString **ToLower** () +- 匹配 + - bool **Equals** (const FString& Other, ESearchCase::Type SearchCase = ESearchCase::CaseSensitive) + - int32 **Compare** ( const FString& Other, ESearchCase::Type SearchCase = ESearchCase::CaseSensitive ) const + - bool **IsNumeric** () + - bool **StartsWith** (const FString& InPrefix, ESearchCase::Type SearchCase = ESearchCase::IgnoreCase) + - bool **EndsWith** (const FString& InSuffix, ESearchCase::Type SearchCase = ESearchCase::IgnoreCase ) + - bool **MatchesWildcard** (const FString& Wildcard, ESearchCase::Type SearchCase = ESearchCase::IgnoreCase) + - bool **Contains** (const TCHAR* SubStr, ESearchCase::Type SearchCase = ESearchCase::IgnoreCase, + ESearchDir::Type SearchDir = ESearchDir::FromStart ) + +- 弹出 + - void **TrimStartAndEndInline** () + - FString **TrimStartAndEnd** () + - void **TrimStartInline** () + - FString **TrimStart** () + - void **TrimEndInline** () + - FString **TrimEnd** () + - void **TrimQuotesInline** (bool* bQuotesRemoved = nullptr); + - FString **TrimQuotes** (bool* bQuotesRemoved = nullptr) + - void **TrimCharInline** (const TCHAR CharacterToTrim, bool* bCharRemoved) + - FString **TrimChar** (const TCHAR CharacterToTrim, bool* bCharRemoved = nullptr) + +- 查找 + - int32 **Find** ( const TCHAR* SubStr, ESearchCase::Type SearchCase = ESearchCase::IgnoreCase, + ESearchDir::Type SearchDir = ESearchDir::FromStart, int32 StartPosition=INDEX_NONE ) + - bool **FindChar** ( TCHAR InChar, int32& Index ) + - bool **FindLastChar** ( TCHAR InChar, int32& Index ) + - int32 **FindLastCharByPredicate** (Predicate Pred, int32 Count) + - int32 **FindLastCharByPredicate** (Predicate Pred) + +- 分片 + - int32 **ParseIntoArray** ( TArray& OutArray, const TCHAR* pchDelim, bool InCullEmpty = true ) + - bool **Split** (const FString& InS, FString* LeftS, FString* RightS, ESearchCase::Type SearchCase, + ESearchDir::Type SearchDir = ESearchDir::FromStart) + +- 分割 + - void **LeftInline** (int32 Count, bool bAllowShrinking = true) + - FString **Left** ( int32 Count ) + - void **LeftChopInline** (int32 Count, bool bAllowShrinking = true) + - FString **LeftChop** ( int32 Count ) + - FString **Right** ( int32 Count ) + - void **RightInline** (int32 Count, bool bAllowShrinking = true) + - FString **RightChop** ( int32 Count ) + - void **RightChopInline** (int32 Count, bool bAllowShrinking = true) + - FString **Mid** (int32 Start, int32 Count) + - void **MidInline** (int32 Start, int32 Count = MAX_int32, bool bAllowShrinking = true) +- 拼接 + - static FString **Join** (const RangeType& Range, const TCHAR* Separator) + - static FString **JoinBy** (const RangeType& Range, const TCHAR* Separator, ProjectionType Proj) +- 替换 + - FString **Replace** (const TCHAR* From, const TCHAR* To, ESearchCase::Type SearchCase = ESearchCase::IgnoreCase) + - int32 **ReplaceInline** ( const TCHAR* SearchText, const TCHAR* ReplacementText, ESearchCase::Type SearchCase = ESearchCase::IgnoreCase ) + - FString **ReplaceQuotesWithEscapedQuotes** () + - void **ReplaceCharWithEscapedCharInline** ( const TArray* Chars = nullptr ) + - FString **ReplaceCharWithEscapedChar** ( const TArray* Chars = nullptr ) + - void **ReplaceEscapedCharWithCharInline** ( const TArray* Chars = nullptr ) + - void **ConvertTabsToSpacesInline** (const int32 InSpacesPerTab); + - FString **ConvertTabsToSpaces** (const int32 InSpacesPerTab) +- 反转 + - void **ReverseString** (); + - FString **Reverse** () + +#### [FText](https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/StringHandling/FText/) + +> 支持文本本地化(多语言),因此面向用户的所有文本都应使用它 + +##### 文本本地化(国际化) + +#### [FName](https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/StringHandling/FName/) + +> 用于命名,它不区分大小写,为便于快速定位某个资源,在创建时,FName会根据字符串内容计算出一个Hash值,这样实现比较逻辑的时候不用比较字符串内容,而是直接比较hash值是否相等。 + +> ### 相互转换 +> +> ```c++ +> TestString = TestName.ToString(); +> TestString = TestText.ToString(); +> +> TestName = FName(*TestString); +> TestName = TestText // 从FText到FName转换不存在,但是可以通过先转到FString,再转到FName,但是不可靠,因为FName不区分大小写。 +> +> TestText = FText::FromName(TestName) +> TestText = FText::FromString(TestString) +> ``` + +#### 路径 + +> FPaths +> +> FPackagePath + +#### 正则表达式 + +> https://blog.csdn.net/justtestlike/article/details/81393221 + +### 数学 + +UE的数学函数位于以下两个命名空间中: + +- [FMath](https://docs.unrealengine.com/4.27/en-US/API/Runtime/Core/Math/FMath/):UE的基础数学库 +- [UkismetMathLibrary](https://docs.unrealengine.com/4.27/en-US/API/Runtime/Engine/Kismet/UKismetMathLibrary/):蓝图库,在FMath的基础上封装了大量函数公开到蓝图 + +### 时间日期 + +### [定时器](https://docs.unrealengine.com/4.26/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Timers/) + +### 文件操作 + +## 输入输出 + +### FArchive + +### 文件 + +UE中通过单例类 **[IFileManager](https://docs.unrealengine.com/4.26/en-US/API/Runtime/Core/HAL/IFileManager/)** ,提供了大量的文件操作,其中包括但不限于: + +- 文件的读写、移动、删除、存在性验证、可读性验证、查找(递归) +- 目录的创建、删除、存在性验证、遍历 + +对于文件IO, **[FFileHelper](https://docs.unrealengine.com/4.27/en-US/API/Runtime/Core/Misc/FFileHelper/)** 中还提供了一些便捷的静态方法,用于文件的一次性读写。 + +### 字节流 + +### 图片 + +### 序列化 + +### Json + +> 详细参考:https://www.cnblogs.com/shiroe/p/14751769.html + +### XML + +## 多线程 + +### FThread、FRunnable + +### FStreamManager + +### AsyncTask + +### TaskGraph + +## 调试 + +### 日志 + +- 自定义日志类别 + + ``` c++ + // 头文件中声明 + DECLARE_LOG_CATEGORY_EXTERN(CustomLogCategory, Log, All); + ``` + + ``` c++ + // 源文件中定义 + DEFINE_LOG_CATEGORY(CustomLogCategory); + ``` + +- 打印日志 + + ``` c++ + UE_LOG(CustomLogCategory,Log,TEXT("This is log %d"),123); + ``` + + - Log + - Warning + - Error + +### 断言 + +> 代码位于:`Engine\Source\Runtime\Core\Public\Misc\AssertionMacros.h` + +- checkCode +- check +- checkf +- checkNoEntry +- checkNoReentry +- checkNoRecursion +- verify +- verifyf +- unimplemented +- ensure +- ensureMsgf +- ensureAlways +- ensureAlwaysMsgf + +### 性能分析 + +### 控制台指令 + +## 建模工具 + +``` c++ +void UBPLibrary::SetStaticMeshPivot(UStaticMesh* InStaticMesh, EStaticMeshPivotType PivotType) +{ + if(InStaticMesh == nullptr) + return; + auto InteractiveToolsContext = NewObject(); + InteractiveToolsContext->InitializeContextWithEditorModeManager(&GLevelEditorModeTools(), nullptr); + UE::TransformGizmoUtil::RegisterTransformGizmoContextObject(InteractiveToolsContext); + + InteractiveToolsContext->ToolManager->RegisterToolType("EditPivotTool", NewObject()); + UStaticMeshComponent* StaticMeshComp = NewObject(); + StaticMeshComp->SetStaticMesh(InStaticMesh); + + InteractiveToolsContext->TargetManager->AddTargetFactory(NewObject()); + UToolTarget* Target = InteractiveToolsContext->TargetManager->BuildTarget(StaticMeshComp, FToolTargetTypeRequirements()); + + InteractiveToolsContext->ToolManager->SelectActiveToolType(EToolSide::Left, "EditPivotTool"); + GLevelEditorModeTools().SelectNone(); + GLevelEditorModeTools().GetSelectedComponents()->Select(StaticMeshComp); + + InteractiveToolsContext->ToolManager->ActivateTool(EToolSide::Left); + if (auto EditTool = Cast(InteractiveToolsContext->ToolManager->ActiveLeftTool)) + { + EditTool->SetTargets({ Target }); + EditTool->RequestAction((EEditPivotToolActions)PivotType); + EditTool->Tick(0.1); + EditTool->Shutdown(EToolShutdownType::Accept); + } +} +``` + +## 开发心得 + +本着 **合理即存在** 的思维方式 去 **抄** diff --git "a/Docs/04-UnrealEngine/3.\347\262\222\345\255\220\347\263\273\347\273\237.md" "b/Docs/04-UnrealEngine/3.\347\262\222\345\255\220\347\263\273\347\273\237.md" new file mode 100644 index 00000000..216b1759 --- /dev/null +++ "b/Docs/04-UnrealEngine/3.\347\262\222\345\255\220\347\263\273\347\273\237.md" @@ -0,0 +1,857 @@ +--- +comments: true +--- + +# 粒子系统(Particle System) + +> 粒子系统(Particle System)表示三维计算机图形学中模拟一些特定的模糊现象的技术,而这些现象用其它传统的渲染技术难以实现真实感的物理运动规律。经常使用粒子系统模拟的现象有火、爆炸、烟、水流、火花、落叶、云、雾、雪、尘、流星尾迹或者象发光轨迹这样的抽象视觉效果等等。 +> +> [《百度百科》— 粒子系统](https://baike.baidu.com/item/%E7%B2%92%E5%AD%90%E7%B3%BB%E7%BB%9F) + +## 原理 + +粒子系统主要适用于渲染 **大量运动的** 物体,它的核心工作流程并不复杂,熟悉图形API的小伙伴都知道,使用GPU去绘制一个物体,需要调用`DrawCall`,因为粒子系统中存在非常多的微小粒子,常规的做法可能是这样: + +``` c++ +for(int i = 0; i < particles.num(); i++){ + drawParticle(particles[i]); +} +``` + +这样做确实能达到效果,但由于大量的`DrawCall`调用,且粒子间存在许多重复数据,会导致粒子系统的渲染消耗非常高,因此所能渲染的粒子数量十分有限。 + +为了能够极大程度的复用粒子数据和减少`DrawCall`,小伙伴们应该都想到了—— **实例化(Instancing)** + +关于实例化的概念和使用,详见: + +- [Learn OpenGL - Instancing](https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/10%20Instancing/) + +通过实例化可以将粒子系统的`DrawCall`进行合并,并且能复用粒子间相同的数据。 + +搞清楚如上概念,可以明白粒子系统只是一个 使用 实例化渲染 的工作流程,从这一维度去思考怎么制作粒子效果,我们考虑的是: + +- 如何构建粒子系统所需的实例化数据(Instance Data)。 +- 如何利用实例化数据去渲染出粒子效果。 + +从这两个角度出发,应该很容易就能搭建出粒子系统的工作管线,下文主要会从架构上去阐述一些粒子系统的细节: + +对于一个简单的粒子,它可能会使用这样的数据结构: + +``` c++ +struct Particle{ //粒子的属性结构 + vec3 position; + vec3 rotation; + float size; + float age; + //... +} +``` + +大多数粒子系统的架构中,会使用一个名为 **粒子发射器(Particle Emitter)** 的结构,它维护着一个存放数据的 **粒子池(Particle Pool)** ,并且负责 **生成(Spawn)** , **更新(Update)** 和 **回收(Recycle)** 粒子,使用 **粒子渲染器(Particle Renderer)** 将粒子池中的数据渲染出来,也就得到了粒子效果,这个过程可以看作是: + +``` c++ +class ParticleEmitter{ +protected: + void Tick() override{ + Spawn(); //生成阶段:创建新粒子 + UpdateAndRecycle(); //更新阶段:更新每个粒子的状态数据并回收已死亡的粒子 + Render(); //渲染阶段:使用粒子渲染器将粒子效果渲染出来 + } +private: + vector mParticlePool; + ParticleRenderer mParticleRenderer; +}; +``` + +### 粒子池 + +因为计算机的内存资源是有限的,所以粒子系统需要保证 **粒子池的占用内存不会随程序运行一直膨胀** ,为此粒子系统往往会通过以下手段避免这个问题: + +- 限制粒子的发射数量。 +- 粒子具备生命周期,对于超出生命周期的粒子,及时进行回收。 + +这也为粒子池提供了内存优化的基础: + +- 已知粒子的发射数量和生命周期,结合当前电脑的最大帧数,很容易估算出该发射器的最大粒子数量,而这个数量的内存往往会一开始就分配给粒子池,从而避免粒子池的内存重分配产生的开销. +- 为了让粒子池能够并行回收(避免当从数组中移除一个元素时,需要挪动后方元素填充到之前的位置),往往会使用两个相同大小的粒子池交替迭代。 + +假如我们想要更新和回收粒子,可能会写出这样的代码: + +``` c++ +vector ParticlePool; +ParticlePool.reserve(10000); //预分配能存放10000个粒子数据的内存 + +int index = -1; +for(int i = 0; i < ParticlePool.size(); i++){ + if(ParticlePool[i].isAlive()){ + index++; + ParticlePool[index] = ParticlePool[i]; + ParticlePool[index].position = ...; + ParticlePool[index].size = ...; + ...; + } +} +ParticlePool.reseize(index); +``` + +它的逻辑复杂度是O(n),如果还要优化,我们可以并行处理for循环,但上面的代码结构却做不到,是因为: + +- 内部的循环逻辑会对外部共有变量`index`进行读写。 +- 对`ParticlePool`的不同区域同时进行读和写,并行会出现乱序执行导致数据混乱。 + +为了解决这个问题,我们一般会用两个`ParticlePool`交替处理,在局部线程中通过原子操作对`index`读写,因此ParticleEmitter的结构代码可能会变成了这样: + +``` c++ +class ParticleEmitter{ +protected: + void InitPool(){ + mParticlePool[0].reserve(...); + mParticlePool[1].reserve(...); + mCurrentPoolIndex = 0; + mNextPoolIndex = 1; + mCurrentNumOfParticle = 0; + } + void Tick() override{ + Spawn(); + UpdateAndRecycle(); + Render(); + } +private: + vector mParticlePool[2]; + int mCurrentPoolIndex; + int mNextPoolIndex; + int mCurrentNumOfParticle; + + ParticleRenderer mParticleRenderer; +}; +``` + +### 生成阶段 + +粒子的生成阶段主要是生成新的粒子存放到粒子池中并初始化,这个过程可以看做是: + +``` c++ +void Spwan(){ + SpwanPerFrame(mParticlePool[mCurrentPoolIndex],mCurrentNumOfParticle,100); //每帧生成100个粒子 +} + +// 发射机制 可自定义 +void SpwanPerFrame(vector& ParticlePool,int& NumOfParticle, int NumOfNewParticle){ + for(int i = 0; i < NumOfNewParticle ; i++ ){ //NumOfNewParticle为新增粒子的数量 + Particle& NewParticle = ParticlePool[NumOfParticle]; + NumOfParticle++; + InitializeParticle(NewParticle); + } +} + +// 初始化机制 可自定义 +void InitializeParticle(Particle& particle){ + particle.position = vec3(0.0f,0.0f,0.0f); + //particle... +} +``` + +> 这里需要注意的是,`Spawn`函数是每帧调用,发射器除了有 **每帧** 发射固定数量的发射机制,还可能是 **每秒** 发射“固定”数量,但由于不同系统的环境下,游戏中每秒的帧数不可预知,因此,对于这类 **根据时段确定发射数量的发射机制** ,其粒子数量是很难精确可控制的。 + +### 更新回收阶段 + +此阶段的伪代码可以看做是: + +``` c++ +void UpdateAndRecycle(){ + int IndexOfNextBuffer = -1; //初始索引 + for_each_thread(int i = 0 ; i < mCurrentNumOfParticle ; i++){ //并行for + const Particle& CurrentParticle = mParticlePool[mCurrentPoolIndex][i]; + if(CurrentParticle.isActive()){ + int CurrentIndex = atomicAdd(IndexOfNextPool,1); //使用原子操作读写外部公共变量 + Particle& NextParticle = mParticlePool[mNextPoolIndex][CurrentIndex]; + UpdateParticleStatus(CurrentParticle,NextParticle); + } + } + CurrentParticlesBufferSize = IndexOfNextBuffer; //更新当前存活的粒子数量 + swap(mCurrentPoolIndex,mNextPoolIndex); //交换当前粒子池的索引 +} + +// 粒子状态更新机制 可自定义 +void UpdateParticleStatus(const Particle& CurrentParticle, Particle& NextParticle){ + NextParticle.position = CurrentParticle.position; + //NextParticle... +} +``` + +### 绘制阶段 + +绘制本身只是利用粒子的数据进行渲染,它的基本过程可以看做是: + +``` c++ +void Render(){ + mParticleRenderer.BindAttribute("position",mParticlePool[mCurrentPoolIndex],offset0,stride0); + mParticleRenderer.BindAttribute("color",mParticlePool[mCurrentPoolIndex],offset1,stride1); + ...; + mParticleRenderer.drawInstancing(); +} +``` + +该阶段体现了粒子的本质:粒子只是一堆数据,粒子系统(发射器)只不过是批量处理这些数据,将这些数据绑定到粒子渲染器的 **属性插槽** 上,从而完成粒子效果的渲染。 + +### CPU VS GPU + +上面提到粒子系统本质上都是围绕着一堆粒子数据进行处理,主要可划分为: + +- **生成阶段** 和 **更新回收阶段** :这两个阶段主要是在处理粒子数据,而Niagara提供了 **可视化脚本** 来为这两个阶段提供自定义操作,最终脚本将会: + - CPU:通过节点拼接的C++代码,会在Niagara的虚拟机中执行。 + - GPU:Niagara会根据节点图,通过 **NiagaraHlslTranslator** 将之翻译为对应的 **HLSL** 代码供 **ComputerShader** 调用。 +- **绘制阶段** :无论是CPU粒子还是GPU粒子,它们都可以使用相同的渲染器,区别在于: **CPU粒子需要将数据上传到GPU上, 而GPU粒子的数据本身就在GPU,无需传输** 。 + +上述过程在逻辑上大同小异,其中需要注意的是GPU粒子的回收,GPU粒子调用ComputeShader **并行** 处理每个粒子,但是在更新会回收阶段,就涉及到数据的重定位,GPU之所以可以能够高效的并行,正是因为每一条分支上的数据互不相通且互不影响,而我们要实现GPU粒子的回收,就必须“打破”这一规定,当下而言,要达到这样的效果主要有两条途径: + +- 图形渲染管线:禁用光栅化,使用TransformFeedback(OpenGL)/ Stream Output(DX),Geometry Shader中回收(不提交)死亡粒子 + + > Geometry Shader本身就带有线性操作,因为这个过程会生成不定数量的新顶点,GPU在执行它写入数据到内存时肯定会加锁重定位,这也是上面为什么会有人使用TransformFeedback和Stream Output制作GPU粒子的原因,当然这也是游戏中需要尽量避免GS的原因。顺带提一下,Stream Output属于DX的正统特性,TransformFeedback在Khronos的待遇就不咋滴了,Vulkan中甚至没有提供官方支持,Metal更是对应功能的API都没有。 + +- 通用计算管线:SSBO Atomic:随着硬件的更新,目前较高版本的渲染驱动都会支持SSBO(Shader Storage Buffer Object),这里我们的关注点主要是:SSBO支持Atomic操作。有了它的加持,就可以在CS中并行的处理粒子,并使用Atomic操作对粒子数据重定位(即上面伪代码中的NewIndex),对于新的粒子数据,则可以通过另一个特性——间接渲染(Indirect Draw):直接利用GPU上的数据调用DrawCall,这样全程都避免了CPU与GPU的粒子传输,下面提供一个简易的GLSL 代码: + + ``` glsl + layout (local_size_x = LOCAL_SIZE) in; + struct Particle { + vec3 position; + vec3 rotation; + vec3 scaling; + vec3 velocity; + float life; + }; + layout(std140,binding = 0) buffer InputParticlesBuffer{ + Particle inputParticles[PARTICLE_MAX_SIZE]; + }; + layout(std140,binding = 1) buffer OutputParticlesBuffer{ + Particle outputParticles[PARTICLE_MAX_SIZE]; + }; + layout(std140,binding = 2) buffer ParticleRunContext{ + int inputCounter; //初始值为当前粒子数 + int outputCounter; //初始化为-1 + float duration; + float lifetime; + }; + + #define inID gl_GlobalInvocationID.x + #define inParticle inputParticles[inID] + + void main(){ //注意:GPU的并行是无序的 + if(inID >= inputCounter||inParticle.life>lifetime) //此处可使用阶跃函数step优化 + return; + uint outID = atomicAdd(outputCounter,1); //atomicAdd是GLSL的原生函数,但需要操作对象来源于SSBO + outputParticles[outID].life = inParticle.life + duration; + } + ``` + +> **着色器存储缓冲区对象([ Shader Storage Buffer Object ](https://www.khronos.org/opengl/wiki/Shader_Storage_Buffer_Object))** 用于提供可读可写的缓冲区数据。 +> +> 它的用法与UniformBuffer几乎一模一样,它们之间的主要区别是: +> +> - **Storage Buffer 可以申请非常大的显存** : OpenGL 规范保证 Uniform Buffer 的大小可以达到 **16KB** ,而 Storage Buffer 可以达到 **128 MB** 。 +>- **Storage Buffer 是可写的,甚至支持原子(Atomic)操作** :Storage Buffer 的读写可能是乱序的,因此它们往往需要增加一些内存屏障来保证同步。 +> - **Storage Buffer 支持可变存储** :这意味着在Storage Buffer中的块(Block),可以定义一个无界数组,就像是 `int arr[];`, 在着色器中可以使用`arr.length`得到数组长度,而在 Uniform Buffer 中的块,在定义数组时需要明确指定数组大小。 +>- **相同条件下,SSBO的访问会比Uniform Buffer要慢** :Storage Buffer 通常像缓冲区纹理一样访问,而 Uniform Buffer 数据是通过内部着色器可访问的内存进行读取。 +> +> 从功能上讲,当通过[Image Load Store](https://www.khronos.org/opengl/wiki/Image_Load_Store)访问时,SSBO 可以被认为是[缓冲区纹理](https://www.khronos.org/opengl/wiki/Buffer_Texture)的更好接口。 +> +> OpenGL提供了一个AtomicCounter的功能,其本质上就是该特性 + +上述完成了GPU粒子的回收,但它其实还带来了一个比较严重的问题——每次回收,粒子的数量会发生变化,但由于数据位于GPU中,CPU中调用`DrawCall`又必须知道这个参数。 + +从GPU上回读数据肯定是不行的,因为现代图形API都是通过指令缓存的方式来加速渲染的,简单点说,就是在某一刻,GPU的指令缓存中可能存在着两帧甚至以上的指令还没处理,如果我们要回读数据的话,就必须保证当前指令被立即执行,也就是说得阻塞渲染线程,等待数据返回,这个操作对整个引擎而言简直就是致命打击,不过好在有解决方法。 + +在OpenGL4.0的更新中,提供了一个新特性——[Indirect Rendering](https://www.khronos.org/opengl/wiki/Vertex_Rendering#Indirect_rendering)(间接渲染,也叫 **Indirect Draw** ),它允许我们使用GPU上的数据调用`DrawCall`,以下是官方对它的定义: + +> [ **Indirect Rendering** ](https://www.khronos.org/opengl/wiki/Vertex_Rendering#Indirect_rendering) +> +> 间接渲染允许直接使用GPU上的数据作为DrawCall参数,例如,[glDrawArrays](https://www.khronos.org/opengl/wiki/GLAPI/glDrawArrays)的参数有原始类型、顶点数和起始顶点,当使用间接绘制命令[glDrawArraysIndirect](https://www.khronos.org/opengl/wiki/GLAPI/glDrawArraysIndirect)时,会从缓冲区对象中获取相应参数。 +> +> 这个功能是为了避免 GPU->CPU->GPU 往返,通过 GPU 决定要渲染的参数,而 CPU 所做的只是决定何时发出绘图命令,以及该命令使用哪个[Primitive](https://www.khronos.org/opengl/wiki/Primitive)。 + +> 在Niagara源码中搜索Indirect,可以找到相关用法的定义,其主要的结构位于: +> +> - `NiagaraVertexFactories\Public\NiagaraDrawIndirect.h` +> - `Niagara\Private\NiagaraGPUInstanceCountManager.cpp` + +#### 优缺点 + +- **CPU** 粒子 + - 优点 + - CPU端可访问完整的游戏环境,可以跟其他粒子或游戏逻辑进行互动。 + - 无需较高的硬件配置,显卡支持可编程管线和实例化渲染即可。 + + - 缺点 + - 只能在CPU端线性地迭代粒子数据,对CPU的性能损耗严重。 + - 粒子数据位于CPU端内存中,渲染时需要统一将粒子数据上传到GPU中,这一传输过程的损耗极其严重。 + +- **GPU** 粒子 + - 优点 + + - 粒子数据全程在GPU端生成和处理,避免了CPU和GPU之间的数据传输。 + - 依靠GPU中的并行单元处理粒子数据,大大降低了粒子处理所需的耗时。 + + - 缺点 + + - GPU粒子的数据位于显存中,无法实时计算包围盒边界,因此我们需要为GPU粒子手动指定固定边界。 + - 不能很好地跟游戏环境交互,但可以通过深度,Scene Capture,距离场去逼近效果。 + - 对硬件配置有一定要求,需支持CS、SSBO Atomic、Indirect Draw(PC端有独显基本没问题,移动端的支持不是特别完善) + +## 粒子系统架构示例 + +在本教程的代码中,也搭建了较为简单的粒子架构,具体请参考: + +> 目前代码示例中还存在一些问题,QRhi并没有直接支持 **Indirect Rendering** ,笔者在此处嵌入了部分原生Vulkan代码,目前还没有改成内存屏障去做同步,而是直接提交次级指令缓冲区,阻塞等待:`mRhi->finish()` + +- 代码:https://github.com/Italink/QEngineUtilities/blob/main/Source/Core/Source/Public/Asset/QParticleEmitter.h +- 示例1——[CPU和GPU粒子](https://github.com/Italink/ModernGraphicsEngineGuide/tree/main/Source/3-GraphicsTechnology/05-GPUParticles/Source/main.cpp) + +![148](Resources/148.gif) + +- 示例2——[景深](https://github.com/Italink/ModernGraphicsEngineGuide/blob/main/Source/3-GraphicsTechnology/07-PostEffect/10-DepthOfField/Source/main.cpp) + +![18](Resources/18.gif) + +## Niagara + +Unreal Engine 的 Niagara 系统非常强大,它本质上是提供了一个 **可编程的数据处理器(CPU or GPU) 和 实例化渲染器** ,从这一层面去思考它,我们可以实现非常多惊艳的效果。 + +### 作用域 + +大多数文章中称此处的作用域为“命名空间”,虽然它确实起着区分变量命名的作用,但对于开发者而言,更为重要的是这些变量作用域所代表的生命周期。 + +以下是UE中Niagara的大体结构,其中 **黄色部分** 代表Niagara中可管理变量的作用域: + +![image-20220704180132630](Resources/image-20220704180132630.png) + +在Niagara编辑器中,你可以通过参数面板来对这些作用域的变量进行增删改 + +![image-20220704181940868](Resources/image-20220704181940868.png) + +- **EngineProvided** :使用该作用域主要是为了从引擎中读取变量,常见的有DeltaTime + +image-20220704182345504 + +- **System Attribute** :单个粒子系统持有的属性变量 + + +![image-20220704183127690](Resources/image-20220704183127690.png) + +- **Emitter Attribute** :单个粒子发射器持有的属性变量 + +- **Particle Attribute** :单个粒子持有的属性变量 + +- **Module Inputs** :模块输入变量,即模块公开给外部设置的变量,它将显示在模块的设置面板: + + +![image-20220705114115160](Resources/image-20220705114115160.png) + +- **Static Switch Inputs** :模块静态分支输入变量,用作编译期优化脚本的逻辑分支。 + +- **Module Locals** :模块本地变量 + +- **Module Outputs** :模块输出变量 + +- **Stage Transients** :阶段临时变量 + +- **User Expose** :用户公开变量,这里里的变量主要是为了公开给Niagara外部的模块(C++或蓝图)调用,在底层上,它与其他变量有着本质上的不同,下文细说。 + +> 关于这些作用域所代表的详细意义,可参阅:[UE4:Niagara的变量与HLSL - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/342315125) + +### 底层核心—FNiagaraDataSet + +Niagara属性本身就是一堆数据,它在代码上对应的结构为 **FNiagaraDataSet** (除了 **User Expose** ),其简要结构如下: + +``` c++ +class FNiagaraDataSet{ + /** Table of free IDs available to allocate next tick. */ + TArray FreeIDsTable; + + /** Number of free IDs in FreeIDTable. */ + int32 NumFreeIDs; + + /** Max ID seen in last execution. Allows us to shrink the IDTable. */ + int32 MaxUsedID; + + /** Tag to use when new IDs are acquired. Should be unique per tick. */ + int32 IDAcquireTag; + + /** Table of IDs spawned in the last tick (just the index part, the acquire tag is IDAcquireTag for all of them). */ + TArray SpawnedIDsTable; + + /** GPU buffer of free IDs available on the next tick. */ + FRWBuffer GPUFreeIDs; + + /** NUmber of IDs allocated for the GPU simulation. */ + uint32 GPUNumAllocatedIDs; + + /** + Actual data storage. These are passed to and read directly by the RT. + This is effectively a pool of buffers for this simulation. + Typically this should only be two or three entries and we search for a free buffer to write into on BeginSimulate(); + We keep track of the Current and Previous buffers which move with each simulate. + Additional buffers may be in here if they are currently being used by the render thread. + */ + TArray> Data; + + /** Buffer containing the current simulation state. */ + FNiagaraDataBuffer* CurrentData; + + /** Buffer we're currently simulating into. Only valid while we're simulating i.e between PrepareForSimulate and EndSimulate calls.*/ + FNiagaraDataBuffer* DestinationData; + + /* Max instance count is the maximum number of instances we allow. */ + uint32 MaxInstanceCount; + /* Max allocation couns it eh maximum number of instances we can allocate which can be > MaxInstanceCount due to rounding. */ + uint32 MaxAllocationCount; +} +``` + +从上面的结构来看, **FNiagaraDataSet** 提供了开篇提到 粒子发射器 基本过程 中所需要的数据结构: + +- 存储分配的最大值(容量,ID) +- `TArray> Data`为两个交替迭代的缓冲区,`FNiagaraDataBuffer* CurrentData`,`FNiagaraDataBuffer* DestinationData`则是指向缓冲器的两个指针,用于快速的Swap + +其中核心的存储结构为 **FNiagaraDataBuffer** ,它的 **关注层面仅仅只是内存数据** ,其简要结构如下: + +``` c++ +class FNiagaraDataBuffer : public FNiagaraSharedObject{ +////////////////////////////////////////////////////////////////////////// + //CPU Data + /** Float components of simulation data. */ + TArray FloatData; + /** Int32 components of simulation data. */ + TArray Int32Data; + /** Half components of simulation data. */ + TArray HalfData; + + /** Table of IDs to real buffer indices. */ + TArray IDToIndexTable; + ////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////// + // GPU Data + /** Location in the frame where GPU data will be ready, for CPU this is always the first group, for GPU is depends on the features used as to which phase. */ + ENiagaraGpuComputeTickStage::Type GPUDataReadyStage = ENiagaraGpuComputeTickStage::First; + /** The buffer offset where the instance count is accumulated. */ + uint32 GPUInstanceCountBufferOffset; + /** GPU Buffer containing floating point values for GPU simulations. */ + FRWBuffer GPUBufferFloat; + /** GPU Buffer containing integer values for GPU simulations. */ + FRWBuffer GPUBufferInt; + /** GPU table which maps particle ID to index. */ + FRWBuffer GPUIDToIndexTable; + /** GPU Buffer containing half values for GPU simulations. */ + FRWBuffer GPUBufferHalf; +#if NIAGARA_MEMORY_TRACKING + int32 AllocationSizeBytes = 0; +#endif + ////////////////////////////////////////////////////////////////////////// + + /** Number of instances in data. */ + uint32 NumInstances; + /** Number of instances the buffer has been allocated for. */ + uint32 NumInstancesAllocated; + /** Stride between components in the float buffer. */ + uint32 FloatStride; + /** Stride between components in the int32 buffer. */ + uint32 Int32Stride; + /** Stride between components in the half buffer. */ + uint32 HalfStride; + /** Number of instances spawned in the last tick. */ + uint32 NumSpawnedInstances; + /** ID acquire tag used in the last tick. */ + uint32 IDAcquireTag; + + /** Table containing current base locations for all registers in this dataset. */ + TArray RegisterTable;//TODO: Should make inline? Feels like a useful size to keep local would be too big. + RegisterTypeOffsetType RegisterTypeOffsets; +}; +``` + +Niagara中持有FNiagaraDataSet的类有: + +- **FNiagaraEmitterInstance** :Niagara发射器实例 + + ```c++ + class FNiagaraEmitterInstance{ + struct FEventInstanceData + { + TArray EventExecContexts; + TArray> EventExecCountBindings; + + TArray UpdateScriptEventDataSets; //更新阶段的数据集 + TArray SpawnScriptEventDataSets; //生成阶段的数据集 + + TArray UpdateEventGeneratorIsSharedByIndex; + TArray SpawnEventGeneratorIsSharedByIndex; + + /** Data required for handling events. */ + TArray EventHandlingInfo; + int32 EventSpawnTotal = 0; + }; + /** particle simulation data. Must be a shared ref as various things on the RT can have direct ref to it. */ + FNiagaraDataSet* ParticleDataSet = nullptr; //粒子数据 + }; + ``` + +- **FNiagaraSystemInstance** :Niagara系统实例 + + ``` c++ + class FNiagaraSystemInstance{ + // registered events for each of the emitters + typedef TPair EmitterEventKey; + typedef TMap EventDataSetMap; + EventDataSetMap EmitterEventDataSetMap; //Niagara系统事件集 + }; + ``` + +- **FNiagaraDICollisionQueryBatch** :包含碰撞事件 + + ``` c++ + class FNiagaraDICollisionQueryBatch{ + FNiagaraDataSet *CollisionEventDataSet = nullptr; + }; + ``` + + +- **FNiagaraScriptExecutionContextBase** :Niagara脚本(Module)执行上下文 + + ``` c++ + struct FNiagaraDataSetExecutionInfo{ + FNiagaraDataSet* DataSet; + FNiagaraDataBuffer* Input; + FNiagaraDataBuffer* Output; + } + struct FNiagaraScriptExecutionContextBase{ + UNiagaraScript* Script; + TArray> DataSetInfo; + } + ``` + +由此可以看出,Niagara中的 **粒子数据** 和 **事件** 都是基于 **FNiagaraDataSet** ,当然这里有个特例就是前面提到的 **User Expose** ,它被存储在 **FNiagaraParameterStore** ,对模块不可见,在Niagara系统中只可以被读取,即通过 **FNiagaraParameterStoreBinding** 绑定到其他属性,使用它主要是为了提供一种Niagara与外部数据的交互手段。 + +在`Niagara\Classes\NiagaraDataSetAccessor.h`,提供了很多内部读写FNiagaraDataSet的模板函数,以Int32为例: + +``` c++ +template +struct FNiagaraDataSetAccessorInt32 +{ + static FNiagaraDataSetReaderInt32 CreateReader(const FNiagaraDataSet& DataSet, const FName VariableName) { } + static FNiagaraDataSetWriterInt32 CreateWriter(const FNiagaraDataSet& DataSet, const FName VariableName) { } +}; +``` + +而Niagara的编辑器层面,并没有直接处理 **FNiagaraDataSet** ,而是对 **FNiagaraVariable** 进行管理编辑,存储到 **FNiagaraDataSetCompiledData** 中,最后进行编译: + +``` c++ +struct FNiagaraDataSetCompiledData +{ + GENERATED_BODY() + + /** Variables in the data set. */ + UPROPERTY() + TArray Variables; + + /** Data describing the layout of variable data. */ + UPROPERTY() + TArray VariableLayouts; + + //... +}; +``` + +至此,Niagara的底层核心就告一段落了,对于细节无需过多深入。 + +### 数据接口—Data Interface + +基本数据类型(数值,向量...)不足以满足所有的应用场景,在有些情况下,粒子系统可能需要访问到很多复杂的数据结构,比如骨骼、纹理、距离场、音频... + +对此,Niagara提供了一种名为 **Data Interface** 的机制专门用来处理复杂的数据结构,在新建任意作用域的属性时,可以在 **DateInterface** 下看到以下条目: + +![image-20220705142208404](Resources/image-20220705142208404.png) + +它在代码底层上对应结构— **UNiagaraDataInterface** + +上面的所有接口均派生自它,这里提它的主要原因是:我们也 **可以在插件派生这个类,进行自定义** 。 + +Niagara源码中有大量的派生示例: + +![image-20220705142910309](Resources/image-20220705142910309.png) + +### 事件机制 + +Niagara的事件机制同样基于上文提到的 **FNiagaraDataSet** + +以 **GenerateLocationEvent** 为例 + +![image-20220705144750986](Resources/image-20220705144750986.png) + +打开它的脚本,你能看到: + +![image-20220705143831980](Resources/image-20220705143831980.png) + +该脚本的核心就是调用了 **LocationEvent_V2 Wirte** 这个节点,这个节点无法被创建,因为它是由引擎序列化产生的,它的节点原型是 **UNiagaraNodeWriteDataSet** ,其结构如下: + +``` c++ +class UNiagaraNodeDataSetBase : public UNiagaraNode +{ +public: + FNiagaraDataSetID DataSet; + TArray Variables; + TArray VariableFriendlyNames; +}; + +class UNiagaraNodeWriteDataSet : public UNiagaraNodeDataSetBase{ + FName EventName; +}; +``` + +再来看它的事件处理器 **Receive Location Event** + +![image-20220705145222454](Resources/image-20220705145222454.png) + +![image-20220705145342871](Resources/image-20220705145342871.png) + +可以看到 **LocationEvent_V2_Read** 与之前的 **LocationEvent_V2 Wirte** 对应,节点原型为 **UNiagaraNodeWriteDataSet** + +其中,Niagara中可自行创建的事件有: + +![image-20220705150245240](Resources/image-20220705150245240.png) + +它们也是通过 **UNiagaraNodeDataSetRead ** / **UNiagaraNodeDataSetWrite ** 对 **FNiagaraDataSet** 进行读写,其执行逻辑很简单,但我们只需要搞清楚: + +- 这两个节点对什么地方的FNiagaraDataSet进行读写? +- 事件什么时候进行处理? + +#### 事件数据的持有者 + +在 **FNiagaraSystemInstance** 的定义中有如下函数: + +``` c++ +class FNiagaraSystemInstance{ + //... + typedef TPair EmitterEventKey; + typedef TMap EventDataSetMap; + EventDataSetMap EmitterEventDataSetMap; //Niagara系统事件集 +}; + +FNiagaraDataSet* FNiagaraSystemInstance::CreateEventDataSet(FName EmitterName, FName EventName) +{ + FNiagaraDataSet*& OutSet = EmitterEventDataSetMap.FindOrAdd(EmitterEventKey(EmitterName, EventName)); + if (!OutSet) { + OutSet = new FNiagaraDataSet(); + } + return OutSet; +} +``` + +Niagara发射器在初始化(可能由重置或重编译导致),会调用如下函数: + +``` c++ +void FNiagaraEmitterInstance::Init(int32 InEmitterIdx, FNiagaraSystemInstanceID InSystemInstanceID){ + const int32 UpdateEventGeneratorCount = CachedEmitter->UpdateScriptProps.EventGenerators.Num(); + const int32 SpawnEventGeneratorCount = CachedEmitter->SpawnScriptProps.EventGenerators.Num(); + const int32 NumEvents = CachedEmitter->GetEventHandlers().Num(); + + if (UpdateEventGeneratorCount || SpawnEventGeneratorCount || NumEvents) + { + EventInstanceData = MakeUnique(); + EventInstanceData->UpdateScriptEventDataSets.Empty(UpdateEventGeneratorCount); + EventInstanceData->UpdateEventGeneratorIsSharedByIndex.SetNumZeroed(UpdateEventGeneratorCount); + int32 UpdateEventGeneratorIndex = 0; + for (const FNiagaraEventGeneratorProperties &GeneratorProps : CachedEmitter->UpdateScriptProps.EventGenerators) + { + FNiagaraDataSet *Set = ParentSystemInstance->CreateEventDataSet(EmitterHandle.GetIdName(), GeneratorProps.ID); + Set->Init(&GeneratorProps.DataSetCompiledData); + + EventInstanceData->UpdateScriptEventDataSets.Add(Set); + EventInstanceData->UpdateEventGeneratorIsSharedByIndex[UpdateEventGeneratorIndex] = CachedEmitter->IsEventGeneratorShared(GeneratorProps.ID); + ++UpdateEventGeneratorIndex; + } + + EventInstanceData->SpawnScriptEventDataSets.Empty(SpawnEventGeneratorCount); + EventInstanceData->SpawnEventGeneratorIsSharedByIndex.SetNumZeroed(SpawnEventGeneratorCount); + int32 SpawnEventGeneratorIndex = 0; + for (const FNiagaraEventGeneratorProperties &GeneratorProps : CachedEmitter->SpawnScriptProps.EventGenerators) + { + FNiagaraDataSet *Set = ParentSystemInstance->CreateEventDataSet(EmitterHandle.GetIdName(), GeneratorProps.ID); + Set->Init(&GeneratorProps.DataSetCompiledData); + + EventInstanceData->SpawnScriptEventDataSets.Add(Set); + EventInstanceData->SpawnEventGeneratorIsSharedByIndex[SpawnEventGeneratorIndex] = CachedEmitter->IsEventGeneratorShared(GeneratorProps.ID); + ++SpawnEventGeneratorIndex; + } + + EventInstanceData->EventExecContexts.SetNum(NumEvents); + EventInstanceData->EventExecCountBindings.SetNum(NumEvents); + + for (int32 i = 0; i < NumEvents; i++) + { + ensure(CachedEmitter->GetEventHandlers()[i].DataSetAccessSynchronized()); + + UNiagaraScript* EventScript = CachedEmitter->GetEventHandlers()[i].Script; + + //This is cpu explicitly? Are we doing event handlers on GPU? + EventInstanceData->EventExecContexts[i].Init(EventScript, ENiagaraSimTarget::CPUSim); + EventInstanceData->EventExecCountBindings[i].Init(EventInstanceData->EventExecContexts[i].Parameters, SYS_PARAM_ENGINE_EXEC_COUNT); + } + } +} +``` + +在这个函数里主要的操作有: + +- 如果发射器中存在事件生成器,则会创建 **FEventInstanceData** + +- 末尾会对所有的事件处理器进行初始化 + +- 代码中将遍历 **CachedEmitter->UpdateScriptProps.EventGenerators** 、 **CachedEmitter->SpawnScriptProps.EventGenerators** 来生成 **EventDataSet** ,追踪它的定义可找到如下代码: + + ``` c++ + void FNiagaraEmitterScriptProperties::InitDataSetAccess() + { + EventReceivers.Empty(); + EventGenerators.Empty(); + + if (Script && Script->IsReadyToRun(ENiagaraSimTarget::CPUSim)) + { + //UE_LOG(LogNiagara, Log, TEXT("InitDataSetAccess: %s %d %d"), *Script->GetPathName(), Script->ReadDataSets.Num(), Script->WriteDataSets.Num()); + // TODO: add event receiver and generator lists to the script properties here + // + for (FNiagaraDataSetID &ReadID : Script->GetVMExecutableData().ReadDataSets) + { + EventReceivers.Add( FNiagaraEventReceiverProperties(ReadID.Name, NAME_None, NAME_None) ); + } + + for (FNiagaraDataSetProperties &WriteID : Script->GetVMExecutableData().WriteDataSets) + { + FNiagaraEventGeneratorProperties Props(WriteID, NAME_None); + EventGenerators.Add(Props); + } + } + } + ``` + + 而它的定义如下: + + ``` c++ + class FNiagaraVMExecutableData{ + UPROPERTY() + TArray WriteDataSets; + }; + ``` + + 其中 **WriteDataSets** 又是由 **FHlslNiagaraTranslator** 通过脚本节点编译时生成的,调用堆栈如下: + + ``` C++ + void UNiagaraNodeWriteDataSet::Compile(...) + { + //... + Translator->WriteDataSet(AlteredDataSet, Variables, ENiagaraDataSetAccessMode::AppendConsume, Inputs, Outputs); + } + + void FHlslNiagaraTranslator::WriteDataSet(...) + { + //... + TMap& Writes = DataSetWriteInfo[(int32)AccessMode].FindOrAdd(DataSet); + } + + FNiagaraTranslateResults FHlslNiagaraTranslator::Translate(...){ + for (TPair > InfoPair : DataSetWriteInfo[0]){ + CompilationOutput.ScriptData.WriteDataSets.Add(SetProps); + } + } + ``` + +至此我们可以得出: + +- 在Niagara Module Script中,只要包含 **UNiagaraNodeWriteDataSet** 就会被定义为事件生成器 +- 事件数据的持有者为 **FNiagaraSystemInstance** + +#### 事件的处理时机 + +在`FNiagaraEmitterInstance::Tick`中,有如下代码: + +``` c++ +void FNiagaraEmitterInstance::Tick(float DeltaSeconds) +{ + //... + if (EventInstanceData.IsValid()) + { + // Set up the spawn counts and source datasets for the events. The system ensures that we will run after any emitters + // we're receiving from, so we can use the data buffers that our sources have computed this tick. + const int32 NumEventHandlers = CachedEmitter->GetEventHandlers().Num(); + EventInstanceData->EventSpawnTotal = 0; + for (int32 i = 0; i < NumEventHandlers; i++) + { + const FNiagaraEventScriptProperties& EventHandlerProps = CachedEmitter->GetEventHandlers()[i]; + FNiagaraEventHandlingInfo& Info = EventInstanceData->EventHandlingInfo[i]; + + Info.TotalSpawnCount = 0;//This was being done every frame but should be done in init? + Info.SpawnCounts.Reset(); + + //TODO: We can move this lookup into the init and just store a ptr to the other set? + if (FNiagaraDataSet* EventSet = ParentSystemInstance->GetEventDataSet(Info.SourceEmitterName, EventHandlerProps.SourceEventName)) + { + Info.SetEventData(&EventSet->GetCurrentDataChecked()); + uint32 EventSpawnNum = CalculateEventSpawnCount(EventHandlerProps, Info.SpawnCounts, EventSet); + Info.TotalSpawnCount += EventSpawnNum; + EventInstanceData->EventSpawnTotal += EventSpawnNum; + } + } + } +} +``` + +上述代码表面了事件处理的逻辑: + +- 发射器在Tick时,遍历所有事件处理器,从 **系统实例ParentSystemInstance** 读取事件的DataSet,并写入到 **FNiagaraEventHandlingInfo** 中,供脚本读取。 + +### 使用建议 + +特效在中追求艺术效果的同时,还必须保证: + +- 粒子系统设置正确的包围盒和可延展性,否则在场景中可能就会出现 **粒子莫名其妙消失** 的问题 +- 严格管控Spawn特效的生命周期,避免`Pool Method` 与生命周期处理操作(预裁剪,可延展性剔除,AutoDestroy)出现冲突。 + +关于优化,需要了解: + +- 如何缩减粒子发射器的数量? + + > 理想的情况是:只要两个发射器所使用的渲染器参数是完全一致的,那么它们就应该使用同一个发射器 + > + > 因为发射器中的一个渲染器对应一条图形渲染管线,粒子系统本质上是处理图形渲染管线所需的实例化数据 + > + > 多一个发射器就会多一份渲染数据,并且要多维护一个粒子缓冲区 + > + > 如果只是想调整粒子的运动机制,可以通过自定义Module的方式进行扩展 + +- 如何界定该使用CPU粒子还是GPU粒子? + + > CPU粒子可以动态地计算包围盒边界,但它只适用于少量的粒子渲染, + > + > GPU粒子可用来渲染大量的粒子数据,但必须为其设置 **准确的包围盒边界** + > + > 界定的分割线主要是粒子数量,如果在千、万级别的粒子数量,必须使用GPU粒子 + > + > 少量粒子,比如几个,建议使用CPU粒子 + +- 为了追求效果,不得以使用CPU粒子时,主要需要考虑两点: + + - CPU粒子的执行是串行的,随着粒子系统复杂度的增加,其性能损耗会成倍增加。 + + - CPU粒子在渲染线程中的性能瓶颈,主要是粒子数据的传输(带宽有限),这也是为什么它并不适用于大量粒子的绘制。 + +- 粒子系统的工作方式不同于常规的Pipeline,如果是放置在场景中的单个物件,你需要考虑使用Actor(Game Object),而不是粒子系统。 +- 除了控制粒子系统的复杂度和数量,还可以通过其他手段来降低粒子的性能消耗: + - 避免粒子拥挤,减少Overdraw + - 限制透明度 + - ... + +- 离线(编辑器时)生成Niagara模拟所需的数据。 +- 善用 Niagara 调试器 和 Insight + +## 相关文章 + +- UE4Niagara系列教程 - https://www.zhihu.com/column/c_1323697556422369280 diff --git "a/Docs/04-UnrealEngine/4.Niagra\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/Docs/04-UnrealEngine/4.Niagra\346\200\247\350\203\275\344\274\230\345\214\226.md" new file mode 100644 index 00000000..5a9155b9 --- /dev/null +++ "b/Docs/04-UnrealEngine/4.Niagra\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -0,0 +1,526 @@ +--- +comments: true +--- + +# Niagara 性能优化 + +**Niagara** 是 **Unreal Engine** 提供的一个几乎无所不能的粒子系统,网上有非常多关于如何使用它来制作某些效果的优质教程,但Niagara的使用管理和性能优化往往会被特效制作人员给忽视。 + +由于笔者长期以来一直被蹂躏,因此本节会对相关内容做一些补充,希望小伙伴在了解之后可以尽可能避免这类问题的发生=.=。 + +## 使用 + +**粒子系统(Particle System)** 是三维场景中用来丰富效果的重要手段,它不同于场景中的 **网格(Mesh)** ,其显著特征在于: + +- 粒子的 **数量** 会比较多 +- 粒子会随着时间 **运动** + +如果不满足这两个特征(即`数量少`且`静态`),那么就需要制作人员考虑是使用 **MeshActor** ,还是粒子。 + +在 UE 中的特效,我们主要可以划分为两大类: + +- **生成特效** :使用`SpawnSystemAtLocation`和`SpawnSystemAttached`在场景中 **动态生成** 的特效。 +- **场景特效** :以 **Actor的形式布置在场景** 中的特效。 + +关于这两类特效,有着不同的关注重点,以及相应的管理措施。 + +### 包围盒 + +在游戏引擎中,为了减少每一帧图像的渲染开销,引擎会将一些不可见的物体给剔除掉,让这些物体不参与 GPU 的渲染 ,如下图所示: + +![img](Resources/CameraCulling.gif) + +> 该图像来自于:[马克·加尔维斯 - 主页 (optus23.github.io)](https://optus23.github.io/) + +如果你了解 [3D视图变换](https://zhuanlan.zhihu.com/p/597152918) 的话,应该会知道:可以通过相机矩阵计算一遍物件的所有顶点,就能确定物件有没有在相机范围之内. + +![img](Resources/viewing-frustum-_frustum.fit_lim.size_768x.gif) + +但显然,一个物体可能具有成千上万的顶点,如果为每个顶点就做一遍计算的话,游戏引擎可能就变成PPT播放器了~ + +为了简化这个过程,游戏引擎都会采用一种取巧的手段,使用一个能够包围物件的简单盒子来代替物件的顶点进行相机剔除的计算,虽然盒子顶点比较少,可视性判断不够精准,但它带来的性能提升是非常可观的。 + +> 在逻辑上可以认定为:只要能看到这个盒子,就一定能证明这个物件是可见的 + +![image-20231007172724563](Resources/image-20231007172724563.png) + +> 显示物体包围盒:`ShowFlag.Bounds 1` + +所以大家要明白一点: **引擎中确定一个物体是否在相机范围内,或者是被遮挡,一般情况下,参考的并不是它本身的顶点信息,而是物体的包围盒。** + +对于 **静态网格体(StaticMesh)** ,它的顶点都是静态的,在创建资产时,引擎就已经为其自动生成了包围盒信息 + +但对于特效而言,由于粒子是动态的,它的包围盒就无法静态生成。 + +在UE中,对于 **CPU粒子** ,引擎在每帧都会重新计算Niagara的包围盒边界: + +cpasd + + + +但 **GPU粒子** 由于其实现的特殊性(所有粒子的数据均在GPU中,且粒子数量往往非常多),从而导致CPU侧无法实时计算粒子的包围盒,开发者 **必须** 为其设置一个 **固定边界** ,即手动设置包围盒的大小 + +> GPU粒子是GPU驱动的渲染管线,整个过程几乎没有跟CPU的交互,而图形渲染管线的数据往往可以看做是单向的:CPU->GPU,虽然也能从GPU上回读数据,但这个过程会存在一定延迟,且性能代价非常高昂) + +在Niagara中,可以在这里设置 **粒子发射器** 的包围盒: + +![image-20231007175720103](Resources/image-20231007175720103.png) + +最简单的方式是为 **整个粒子系统** 设置包围盒: + +![image-20231007180002716](Resources/image-20231007180002716.png) + +**制作人员必须保证包围盒的正确性** (包围盒尽可能以最小的区域涵盖整个特效的运动空间),否则会造成以下后果: + +- 如果特效的包围盒与实际的运动空间不匹配,则会导致错误的视觉裁剪: + +cpsadasd + +- 如果特效的包围盒太大,将导致明明没有在视野范围内的特效,无法被剔除,依旧会执行渲染,造成不必要的损耗。 + + image-20231007181531935 + +### 生命周期 + +对于 **场景特效** ,它的生命周期由场景从Actor层面进行管控,一般情况下无需我们关心,而 **生成特效** 的生命周期管控,有一些我们必须要了解的细节: + +**生成特效** 是指使用 **UNiagaraFunctionLibrary** 下的几个函数生成的特效: + +``` c++ +UNiagaraComponent* SpawnSystemAtLocation(const UObject* WorldContextObject, + class UNiagaraSystem* SystemTemplate, + FVector Location, + FRotator Rotation = FRotator::ZeroRotator, + FVector Scale = FVector(1.f), + bool bAutoDestroy = true, + bool bAutoActivate = true, + ENCPoolMethod PoolingMethod = ENCPoolMethod::None, + bool bPreCullCheck = true); + +UNiagaraComponent* SpawnSystemAttached(UNiagaraSystem* SystemTemplate, + USceneComponent* AttachToComponent, + FName AttachPointName, + FVector Location, + FRotator Rotation, + FVector Scale, + EAttachLocation::Type LocationType, + bool bAutoDestroy, + ENCPoolMethod PoolingMethod, + bool bAutoActivate = true, + bool bPreCullCheck = true); +``` + +**SpawnSystemAtLocation** 用于在指定位置生成粒子组件,而 **SpawnSystemAttached** 用于附着在另一场景组件中,对于生命周期,我们主要需要关注几个参数: + +- **bAutoActivate** :是否自动激活,true表示随后立即播放粒子效果 +- **bAutoDestroy** :是否自动销毁,true会在粒子播放完成后销毁组件 +- **bPreCullCheck** :预剔除,如果为true,生成粒子系统的包围盒不在视椎体范围内,那么将不会生成粒子组件,即返回为null +- **PoolingMethod** :池操作,复用粒子组件的机制,它的值可以是: + - **None** + - **AutoRelease** :自动释放,播放结束之后,自动将之存放到特效池中以供复用 + - **ManualRelease** :手动释放,需要手动调用 **ReleaseToPool** 才能使该粒子组件被复用 + - ManualRelease_OnComplete:非公开选项,用于Niagara内部的状态控制,请不要使用它。 + - FreeInPool:非公开选项,用于Niagara内部的状态控制,请不要使用它。 + +#### 特效池 + +> 详细机制请参阅文章:[【UE4】特效池 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/394772587) + +##### 缓存机制 + +特效池的目的主要是为了能够复用粒子组件,它的缓存机制可以看做是: + +- 如果生成参数带有池操作(即`PoolingMethod != ENCPoolMethod::None`),那么Niagara会到特效池中去搜索是否有可复用的同类型组件,如果有,则使用它,否则就会新建粒子组件,并记录它的使用时间。 +- 新建的粒子组件不会被释放( **意味着bAutoDestroy会失效** ),它会被存到特效池中进行管理。 +- 对于 **AutoRelease** 的粒子组件,当播放结束时,特效池会标记它为可复用的。 +- 对于 **ManualRelease** 的粒子组件,调用 **ReleaseToPool** 时,如果该粒子已经播放完成,会立即标记为可复用,否则修改池操作为 **ManualRelease_OnComplete** ,在播放完成时,也会标记复用。 + +##### 清理方式 + +**UNiagaraComponentPool** 拥有定时清理的机制,两个主要关键参数为: + +- **GNiagaraSystemPoolKillUnusedTime** :默认为180 +- **GNiagaraSystemPoolingCleanTime** :默认为30 + +上述配置对应的自动清理的机制可以大致当做是:UNiagaraComponentPool每30秒会去清理一下超过180s未被复用的粒子组件 + +> 实际上并不是Tick清理,而是在每一次复用的时候进行清理,所以未必是30s,可能偏差会比较大。 + +在切换World时, **UNiagaraComponentPool** 会执行Cleanup + +也可手动调用 **UNiagaraComponentPool::ClearPool(UNiagaraSystem* System)** 进行清理。 + +#### 坑点 + +- 如果有池操作( **PoolingMethod** ),那么 **bAutoDestroy** 将会失效 +- 池机制正常工作的前提是粒子系统能够播放结束,请注意: + - 确保粒子系统中没有生命周期无限的发射器 + - 调用了ResetParticles(true),会走不到 Complete + - 如果可延展性剔除的 **剔除反应为休眠** ,被 **剔除的特效不会被复用** ,且 **OnSystemFinished不会触发** (从严格意义上并不算完成) +- 对于有池操作的粒子组件,请 **不要调用DestoryComponent** ,池中的粒子组件会在切换World时清理,或者定时清理。 +- 如果 **SpawnSystemAttached** 带有池操作,那么新生成的粒子组件的 **真实持有者为World,而非附着的组件** ,粒子组件将 **不会随附着组件销毁而销毁** +- 对 **AutoRelease** 的粒子组件调用 **ReleaseToPool** 会有报错 +- 对于即没有开启 **AutoRelease** ,也没有池操作的粒子,需要严格管控它的生命周期 + +### 可延展性 + +通过设置包围盒,可以在 **在视觉上** 将粒子剔除,但需要注意的是:粒子系统不同于普通的场景物体,它还有 **模拟的成本** ! + +这是目前非常多特效师在使用Niagara容易忽略的问题,很多小伙伴以为看不到粒子时,粒子就会停止,但实际并不是这样的,它依旧在执行: + +![dasd](Resources/dasd.gif) + +对于生成特效,由于它具备一定的生命周期,因此在大多数情况下,它的模拟消耗都无关痛痒。 + +但对于场景特效,如果布置在场景中的NiagaraActor数量过多,对整个程序的影响都非常大。 + +为了解决这个问题,UE提供了一个名为 **效果类型(NiagaraEffectType)** 的资产,用于控制 Niagara System **在模拟上的优化策略** 。 + +![image-20231007193640734](Resources/image-20231007193640734.png) + +我们可以在此处指定NiagaraSystem所使用的效果类型: + +![image-20231007193928904](Resources/image-20231007193928904.png) + +使用它能完成以下需求: + +- 限定粒子系统的最远显示距离 +- 限制当前视口内显示的最大粒子系统的数量 +- 根据性能预算来调整粒子的效果 +- 调整发射器发射数量的缩放系数 + +它的配置面板如下: + +![image-20231007194409696](Resources/image-20231007194409696.png) + +我们可以添加 系统和发射器可延展性配置,每一个配置对应不同性能级别下,所采取的可延展性策略: + +![image-20231007194552208](Resources/image-20231007194552208.png) + +它的参数意义如下: + +- 运行针对本地玩家进行剔除:是否允许剔除由本地玩家生成,附加,创建的特效 + +- 更新频率:`仅生成`、`低`、`中`、`高`、`连续` + + **NiagaraEffectType** 和 **NiagaraComponent** 一样,都是由 **NiagaraWorldManager** 进行管理, **NiagaraEffectType** 会单独进行Tick,这里的更新频率是指它去检测 **NiagaraSystem** 当前状态的频率,频率越高,剔除反应越及时,但性能开销也越大。 + +- 剔除反应:`摧毁`、`摧毁并清除`、`休眠`、`休眠并清除` + + 清除会立刻被杀死所有粒子,然后剔除NiagaraSystem,如果不清除,则不会生成新粒子,等待已存在粒子生命周期全部结束之后,再进行剔除 + + 摧毁与休眠的区别在于,摧毁是直接将粒子销毁,而休眠可以当作只是把粒子隐藏等待激活。 + + 对于场景特效,由于它是放置在场景中的Actor,销毁之后不会再生成,所以一般设置为休眠,具体的销毁和加载由大世界进行管理。 + + 而对于生成特效,他一般是一次性生成的,如果设置为休眠,它会一直占用内存,所以一般会将其销毁。 + + 在UE 5.3中,增加了 `暂停` 机制, 它与休眠的区别在于,暂停不会重新激活,将会保留粒子的当前数据(占用内存) + +- 重要性处理器:`年龄`、`距离` + + 重要性处理器主要跟下方的 **最大系统实例** 有关,最大系统代理决定了当前只允许多少个同类型粒子系统的实例存在,而重要性处理器,则是确定粒子排序的优先级,假如以距离为目标,有十个粒子系统,最大系统实例为五,那么 **NiagaraEffectType** 就只会保留最近的五个粒子效果。 + +- 系统延展性设置 + + **设置列表** ,每添加一个设置项,可以配置该项在哪些环境下才生效 + + ![image-20231007194731440](Resources/image-20231007194731440.png) + + - 最大距离:根据特效与当前玩家的距离来剔除粒子 + + - 剔除代理模式:`已实例化渲染`,如果开启了 **已实例化渲染** ,将会创建一个不可见的粒子系统,其他所有被剔除的同类型粒子系统将使用它进行渲染,这样可以避免大量的模拟成本 + + - 最大系统代理:最大的系统代理数 + + - 预算调整:根据一定预算来调整粒子系统的可延展性策略 + - 可视性剔除:根据可见性来进行剔除。 + +> 具体的说明详见编辑器面板的Tooltip + +对于场景特效,笔者非常建议大家统一对所有场景特效都统一设置这样的效果类型: + +![image-20231007195927854](Resources/image-20231007195927854.png) + +该配置达到的效果是:当粒子系统不可见时,既会停止渲染,也会停止模拟 + +![dasssd](Resources/dasssd.gif) + +它能满足绝大多数的需求。 + +当某些特效需要专门配置剔除策略时,Niagara支持对单独的 NiagaraSystem 和 NiagaraEmitter 进行 **可延展性重载** ,它能 **覆盖** NiagaraEffectType 的配置: + +![image-20231007200945698](Resources/image-20231007200945698.png) + +![image-20231007201137432](Resources/image-20231007201137432.png) + +关于 **Niagara Effect Type** 详细配置的示例,可以参考:https://www.youtube.com/watch?v=-P3yaZNgeg0 + +### LOD + +Niagara提供了LODDistance的内置变量,我们可以依据它的数值,对Niagara的性能参数进行一些距离上的适配 + +![image-20231008160522818](Resources/image-20231008160522818.png) + +比如,通常会对生成数量制作LOD: + +![image-20231008145838367](Resources/image-20231008145838367.png) + +![sdsssd](Resources/sdsssd.gif) + +### 暖场时间 + +对于放置在场景中的循环特效 ,它往往会有一段`”预热“`的时间,在Niagara中,我们可以设置系统的暖场时间来把这段预热过程给掐掉: + +![image-20231008150524786](Resources/image-20231008150524786.png) + +对于具有 可视性裁剪 的场景特效,休眠裁剪 会重新激活粒子,这个时候会重新播放预热过程: + +![dfgdasdd](Resources/dfgdasdd.gif) + +但设置 暖场时间 把 预热过程 裁剪掉之后,效果是这样的: + +![dfgdasdd1](Resources/dfgdasdd1.gif) + +### 管控发射器数量 + +理想的情况是:只要两个发射器所使用的渲染器参数是完全一致的,那么它们就应该使用同一个发射器 + +因为发射器中的一个渲染器对应一条图形渲染管线,粒子系统本质上是处理图形渲染管线所需的实例化数据 + +多一个发射器就会多一份渲染数据,并且要多维护一个粒子缓冲区 + +如果只是想调整粒子的运动机制,可以通过自定义Module的方式进行扩展 + +### 界定CPU粒子和GPU粒子 + +CPU粒子:适用于少量的粒子渲染,由于数据位于CPU中,因此能获取到整个场景的信息,可以很方便地跟游戏逻辑进行交互,支持粒子间的事件通信,精准的物理碰撞,还可以动态地计算包围盒边界。 + +GPU粒子:有着非常低的性能损耗,适合用来渲染大量(十万级别)的粒子。 + +在界定时,首先思考的维度是粒子数量,如果是粒子数量非常少(比如几个或几十个),那么最好使用CPU粒子(因为没必要为了几个粒子增加GPU的调度开销) + +当数量超过一千时,我们考虑的是:功能需求是否需要用到CPU粒子的特性,如果不需要,那么优先使用GPU粒子。 + +当粒子数非常多,但又需要使用CPU粒子特性时,需要在较低级别的代码维度去扩展GPU粒子的功能。 + +## 优化 + +关于性能优化,我们大多数时候追求的是:在满足美术需求的同时,不造成性能上的卡顿。 + +整个指标非常模糊,对于大多数初学者而言,我们能做的,只有亡羊补牢。 + +所以第一点至关重要,我们需要能够发现性能问题,才能进一步去制定优化计划。 + +### 性能定位 + +UE提供了非常多的指令和工具来检测场景的执行性能,以下视频可作为参考: + +- [[官方培训\]22-UE资产优化 | Epic 肖月_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1FT411x7hU/?spm_id_from=333.337.search-card.all.click) +- [[UnrealCircle苏州\]UE4性能优化 | 周华 苏州谜匣数娱_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1FV411G72Q/?vd_source=e173296de020f10e8476ed443bbdfaed) +- [【搬运】UnrealFest2023 UE5性能优化_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1Yj41147dh/?vd_source=e173296de020f10e8476ed443bbdfaed) + +对于引擎人员,一般需要能够熟练使用 **RenderDoc** , **Unreal Insight** ,各类Stat指令,从这些地方,我们可以获取到非常精细的性能数据。 + +而对于Niagara,UE中提供了专门的工具来监控Niagara的执行情况 —— **Niagara调试器(Niagara Debugger)** + +我们可以在此处打开NiagaraDebugger: + +![image-20231007203244212](Resources/image-20231007203244212.png) + +也可以在NiagaraSystem的细节面板中启动调试器: + +![image-20231007211256346](Resources/image-20231007211256346.png) + +#### 调试Hud + +在`调试Hud`页面中,提供了许多可供配置的Niagara调试视图。 + +勾选 `调试视图已启用`,我们能看到当前场景中正在执行的所有粒子系统的粒子数量和内存 + +![image-20231007203838084](Resources/image-20231007203838084.png) + +可以勾选 `系统显示边界` 来显示NiagaraSystem的包围盒: + +![image-20231007204141184](Resources/image-20231007204141184.png) + +勾选 `显示系统属性`,并添加 属性匹配字符串 ,来增加粒子系统的调试预览视图: + +![image-20231007204536387](Resources/image-20231007204536387.png) + +同样也可以预览粒子属性: + +![image-20231007204851243](Resources/image-20231007204851243.png) + +当场景中粒子系统比较多时,为了排除其他粒子调试视图的干扰,我们可以设置 命名过滤 来选择调试器需要关注的粒子系统: + +![image-20231007205104536](Resources/image-20231007205104536.png) + +> 该配置只关注命名以`New`开头的粒子系统 + +#### 视效大纲视图 + +视效大纲用于对场景中的Niagara进行 **截帧** ,它支持三种视图模式: + +![image-20231007205814852](Resources/image-20231007205814852.png) + +点击 捕获(Capture) 就能截取 Niagara 的执行情况(可配置截取的延迟时间和帧的数量): + +![image-20231007210218516](Resources/image-20231007210218516.png) + +状态模式下,我们可查看场景中所有粒子系统的激活情况,模拟类型(CPU/GPU),粒子数量: + +![image-20231007210228693](Resources/image-20231007210228693.png) + +性能模式下,可进一步查看粒子系统在游戏线程和渲染线程的执行消耗 + +![image-20231007210313044](Resources/image-20231007210313044.png) + +调试模式下,我们可以获取到粒子系统执行时的具体粒子数据: + +![image-20231007210419151](Resources/image-20231007210419151.png) + +#### 性能 + +性能视图提供了一些测试手段: + +![image-20231007210636923](Resources/image-20231007210636923.png) + +#### 调试生成 + +调试生成 就是提供了一个界面去Spawn特效粒子,只需配置完粒子系统之后点击`生成系统`(需要在PIE模式下) + +![image-20231007211058328](Resources/image-20231007211058328.png) + +#### 模块性能 + +Niagara的编辑器中可以获取到每个模块的耗时情况: + +![image-20231007211513307](Resources/image-20231007211513307.png) + +#### 资源占用 + +可以在资产的内容菜单中,点击`尺寸贴图`查看引用贴图的大小: + +![image-20231008101627932](Resources/image-20231008101627932.png) + +![image-20231008101743881](Resources/image-20231008101743881.png) + +#### 问题及建议 + +- 在 Unreal Engine 5.1版本之前,Niagara System存在 **重编译BUG** ,它的表现效果是:首次在游戏中生成粒子时,会触发整个粒子系统的重编译,这是由于在 Niagara System Editor 中进行资源保存,存储了与游戏模式下 不同的一些配置,导致粒子系统的HashID不一致,从而引起编译缓存的失效,因此如果需要评估粒子系统在游戏环境中的执行性能,笔者建议在打包环境下进行测试。 +- Unreal Engine 为 Niagara 提供了非常完善的配套工具,但对于一个大型团队而言,它是易于使用,但不易于管理的,笔者非常建议在上述工具集的基础上,进行进一步的扩展,增加自动化流程,来完成宏观的粒子系统管理。 +- 对于一些常规的渲染优化,如优化材质,简化模型,降低Overdraw,对Niagara同样适用。 + +### 制作优化 + +在亡羊补牢的过程中总结出来的优化经验是弥足珍贵的,它能让制作人员逐渐地了解怎样才能制作出性能卓越的粒子特效。 + +以下是一些能够帮助特效师优化制作的心得(主要针对GPU粒子) + +#### 算力认知 + +`1920 x 1080`是当下电脑显示器常见的分辨率,这代表了屏幕上有将近 **两百万** 个像素点,也意味着电脑绘制一帧图像至少需要做两百万次像素级别的数值处理,而目前一些中端硬件设备上,可以轻易在一秒内绘制成百上千张图像,这惊人的算力得益于 图形处理单元(GPU)的高速发展,与 中央处理单元(CPU)不同的是,CPU是电脑的调度中心,它的架构需要兼顾系统的方方面面,而GPU则是一种剑走偏锋的做法,它的架构中塞入了大量的计算单元以满足图形运算的需求。 + +> 以当下尖端的消费级显卡 `GeForce RTX 4090` 为例,它具有七百六十多亿个晶体管、一万六千多个计算核心。 + +![GPU VS CPU](Resources/format,png.png) + +在清楚这个上限之后,我们可以简单将 `单个GPU粒子` 跟 `像素点` 进行对应,也就是通常情况下,中端显卡也能轻松模拟百万级别数量的粒子效果。 + +但需要注意的是,百万级别往往是整个系统的上限,我们通常不可能将所有性能预算都留给粒子系统,所以在一般情况下,如果整体的粒子数量超过十万,就可能会对整个渲染系统造成性能压力。 + +如果将这十万粒子转换为对应数量的`1x1`的像素点进行平铺,它大约对应了`300x300`尺寸大小的图像: + +![image-20231008104748978](Resources/image-20231008104748978.png) + +粒子系统的 **占屏面积** 越接近它的 **平铺面积** ,则意味着它的性能越好(因为有更少的OverDraw) + +以下是一个平平无奇,但却具有14万粒子数量的NiagaraSystem,它辜负了这么大数量所应匹配的表现效果: + +![image-20231008105515353](Resources/image-20231008105515353.png) + +并且随着视野的远离,它的OverDraw越来越严重: + +![dasssssd](Resources/dasssssd.gif) + +> 这个问题可以使用LOD曲线根据距离来优化粒子数量。 + +#### 底层原理 + +Niagara本身是UE提供的一个可编程的数据处理器,在NiagaraEditor中,我们可以自由管控这个处理器的处理结构: + +![image-20231008110203395](Resources/image-20231008110203395.png) + +并在各个 **阶段(Stage)** 中,使用引擎提供的 可视化节点 编写相应的 **模块(Module)** ,来制定数据的处理逻辑: + +![image-20231008110433919](Resources/image-20231008110433919.png) + +这些可视化的节点逻辑将会被翻译为ComputerShader的代码: + +![image-20231008111218076](Resources/image-20231008111218076.png) + +而数据处理的结果,将绑定到渲染器对应的属性插槽,供给渲染器来完成粒子效果的最终表达: + +![image-20231008110706037](Resources/image-20231008110706037.png) + +从数据处理层面来进行思考,我们的关键目标是怎样制作一个高效的数据处理器,因此掌握Niagara底层模块的原理,对整体性能和效果的把控,至关重要。 + +这里有一个非常优秀的教程: + +- [[官方培训\]09-UE粒子基础 | 肖月 Epic bilibili](https://www.bilibili.com/video/BV1ea411V76f/?vd_source=e173296de020f10e8476ed443bbdfaed) + +#### 序列帧烘培 + +Niagara Editor 支持将粒子特效烘培为序列帧,对于一些固定视角的特效,我们可以采取此方案来获得极大程度的优化。 + +![image-20231008151631152](Resources/image-20231008151631152.png) + +![image-20231008151748551](Resources/image-20231008151748551.png) + +#### 程序化内容生成 + +粒子系统主要 **适用** 于模拟 **大量** **动态** 的物体,但只有一些特定需求下,我们才不得不使用粒子系统来进行 **实时的模拟** : + +- 制作可交互的动态效果 +- 需要营造无视觉死角的立体效果 +- 非常大数量的大范围集群 + +排除以上的使用场景,我们通常可以通过 **烘培静态材质** 来达到想要的表现效果。 + +所以我们在制作粒子效果时,需要认真审视:是否需要用粒子系统来进行实时的模拟? + +如果要,那么我们再思考:是否可以将,粒子系统中某些实时计算的阶段,换成已经预制的静态数据。 + +比如在制作跟场景相关的特效时,粒子的模拟可能会需要一些场景信息作为输入,而这些场景数据绝大多数情况下都是静态的,我们没必要在运行时去实时计算场景的相关信息,在编辑器中就能静态生成这些数据,这样就可以以较少的性能成本制作出跟场景高度融合的特效,这在粒子系统数量比较多的情况下,提升尤为明显。 + +我们也可以在Niagara上制作 **程序化内容生成(PCG)** 的资产: + +- 在 **Niagara System** 的 **用户参数(User Parameters)** 中,添加粒子系统所需要的场景数据(这里添加了一个名为`SpawnPositionArray`的变量): + +![image-20231008115550644](Resources/image-20231008115550644.png) + +- 创建一个新的 **Actor 蓝图** ,添加 **NiagaraComponent** ,并指定 **Niagara System** + +- 在蓝图的 **BeginPlay** 事件中去设置我们在Niagara中添加的参数,并将参数输入 `提升为变量` (`Partcile Spawn Position Array`): + + ![image-20231008114722968](Resources/image-20231008114722968.png) + +- 在 蓝图中 添加一个函数(例如叫 `CalcParticleSpawnPositionArray`),并勾选 **Call In Editor** ,在这个函数中获取场景信息(无需关心性能,可以扫描Actor,打射线...),填充蓝图变量值(`Partcile Spawn Position Array`): + + ![image-20231008115241436](Resources/image-20231008115241436.png) + +- 将Actor蓝图放到场景中,可以发现它的细节面板中,之前创建的函数变成了一个可点击的按钮: + + ![image-20231008115733236](Resources/image-20231008115733236.png) + +- 点击按钮将执行对应的函数逻辑,它将会将执行并填充蓝图属性,这些属性值将会在保存时被序列化,当正常启动游戏时,执行BeginPlay,将会把这些数据提交给Niagara + +- 之后可以在Niagara中编写相应的Module来使用这部分数据。 + +> 需要注意的是,我们不能直接在`CalcParticleSpawnPositionArray`去设置Niagara的参数值,而是借助蓝图的变量属性序列化来存储场景数据。 +> +> 这是因为 设置Niagara参数的函数(以`NiagaraSet...`开头),是直接将数据上传给Niagara的数据处理器,而不会进行存储,因此通过函数来设置Niagara参数,在NiagaraSystem的细节面板上不一定能看到参数发生变化(有一部分属性类型是做了编辑器数据的返回同步,所以能在细节面板看到变更) + + + diff --git "a/Docs/04-UnrealEngine/5.FluidNinja\346\265\201\344\275\223\346\217\222\344\273\266.md" "b/Docs/04-UnrealEngine/5.FluidNinja\346\265\201\344\275\223\346\217\222\344\273\266.md" new file mode 100644 index 00000000..d988f336 --- /dev/null +++ "b/Docs/04-UnrealEngine/5.FluidNinja\346\265\201\344\275\223\346\217\222\344\273\266.md" @@ -0,0 +1,425 @@ +--- +comments: true +--- + +# Fluid Ninja + +**Fluid Ninja** 是一个令人惊叹的交互流体模拟插件,使用它可以以极低的性能损耗制作出令人叹为观止的效果: + +![image-20231114145533712](Resources/image-20231114145533712.png) + +![image-20231114145942912](Resources/image-20231114145942912.png) + +- 作者YouTube:https://www.youtube.com/c/AndrasKetzer +- 虚幻商城:https://www.unrealengine.com/marketplace/en-US/product/fluidninja-live +- 讨论社区(Discord):https://discord.com/invite/rgEtwua2tu + +作者Youtube上很多视频教程: + +- [FluidNinja LIVE - ALL IN ONE TUTORIAL and making FX from scratch - YouTube](https://www.youtube.com/watch?v=vXalfRAnXak&t=3869s) + +- [FluidNinja 中文教学 - 陶仁贤 ](https://space.bilibili.com/22866318/channel/seriesdetail?sid=2277642) + +## 获取 + +对于学生用户,可以到 Fluid Ninja 的`Discord社区`中下载学生免费版: + +![image-20231114152013980](Resources/image-20231114152013980.png) + +对于商业用户,请在虚幻商城中购买FluidNinja插件: + +- https://www.unrealengine.com/marketplace/en-US/product/fluidninja-live + +## 示例 + +在`Content/FluidNinjaLive/Tutorial/Levels/`中有大量的示例关卡,这些关卡中都有详细的描述文本: + +![image-20231114161611114](Resources/image-20231114161611114.png) + +最好的学习方式就是依次对这些关卡进行复刻,理解它的参数配置: + +![image-20231117162611951](Resources/image-20231117162611951.png) + +## 关键蓝图 + +### NinjaLiveComponent + +NinjaLiveComponent是FluidNinja的核心,使用它必须搭载一个静态网格体(一般是面片,称它为 **跟踪网格(Trace Mesh)** ),流体模拟的最终结果将作为跟踪网格的材质进行输出: + +![image-20231114162146791](Resources/image-20231114162146791.png) + +> 上图中的红色面片也就是跟踪网格 + +### NinjaLive + +NinjaLive是Fluid Ninja提供的一个默认Actor,它对 NinjaLiveComponent 的参数进行了一层包裹,包含了一个能满足绝大多数情况的使用需求的跟踪面片,简化了NinjaLiveComponent的设置,并添加了一些额外的功能,比如: + +- 通过ActivationVolume 对 NinjaFluid 进行剔除休眠,进入到ActivationVolume才会激活NinjaLiveComponent +- 通过InteractionVolume更细粒度地设置交互区域。 + +![image-20231114164608493](Resources/image-20231114164608493.png) + +### NinjaLive_Utilities + +NinjaLive_Utilities(蓝图Actor)提供了一些便捷的调试功能,比如: + +- 自动控制距离该蓝图最近的玩家 +- 显示鼠标 +- 调整抗锯齿,景深,运动模糊的质量 +- 解除编辑器120FPS的限制 +- ... + +可以将该蓝图拖入场景中,并进行配置: + +![image-20231114165051217](Resources/image-20231114165051217.png) + +### NinjaLive_PresetManager + +NinjaLive_PresetManager(蓝图Actor)用于调整NinjaLive的参数预设。 + +![image-20231114165515082](Resources/image-20231114165515082.png) + +将它拖入场景,启动PIE,可以看到如下面板: + +![image-20231114165552571](Resources/image-20231114165552571.png) + +### NinjaLive_MemoryPoolManager + +NinjaLive_MemoryPoolManager(蓝图Actor)用于配置FluidNinja的内存策略。 + +![image-20231114165855058](Resources/image-20231114165855058.png) + +## 模拟过程 + +在FluidNinja的资产目录下,有一个名为 `Help` 蓝图: + +![image-20231114160604396](Resources/image-20231114160604396.png) + +它里面详细描述了 Fluid Ninja 模拟的流程图: + +![image-20231114161417804](Resources/image-20231114161417804.png) + +NinjaFluid的核心在于: + +- 构建 **Density(密度)** 贴图和 **Velocity(速度)** 贴图 +- 叠加 **噪声(Noise)** 来增加流体的随机度 +- 调整 **画刷(Brush)** 来控制流体的形体 +- 结合 **输出材质(Output Mateial)** +- 将最终结果输出到 **跟踪网格(Trace Mesh)** 的材质表面 +- 得到我们想要的结果: + +![12332312](Resources/12332312.gif) + +- Density 为单通道纹理,值域为[0,1],用于表示当前位置流体的密度。 +- Velocity 为双通道纹理,值域为[0,1],用于表示当前位置流体的矢量速度,由于被标准化为[0,1],速度0实际上对应的RG值为(0.5,0.5),上图中: + - Velocity纹理的R通道对应水平速度,当值`<0.5`时流体向左,`>0.5`时流体向右 + - Velocity纹理的R通道对应竖直速度,当值`<0.5`时流体向上,`>0.5`时流体向下 +- FluidNinja允许输入三通道材质,其中RG通道代表Velocity,B通道代表Density + +## 使用流程 + +制作过程中,我们本质上是对 **NinjaLiveComponent** 进行操作,它的 **配置项分类(Categories)** 有: + +![image-20231114195314683](Resources/image-20231114195314683.png) + +- `Live Activation` :只有一个Disable Component选项,可以通过蓝图去动态设置来进行剔除。 +- `Live Interaction` :交互相关的所有配置,比如筛选交互目标,跟踪网格的朝向、锁定,世界坐标偏移等。 +- `Live Brush Setting` :画刷的参数配置。 +- `Live Generic` :通用配置,主要是调整效果的预设、输入和输出。 +- `Live Performance` :性能配置,可配置效果的迭代次数,FPS范围限制,LOD等。 +- `Live Compatibility` :兼容性配置,可配置碰撞通道,覆盖输入等。 +- `Live Debug` :调试配置,提供了一些勾选项用于打印提示信息。 +- `Live Raymarching` :配置Raymarching。 +- `Live Memory Management` :配置内存管理。 + +### 构建输入 + +这里的输入主要指的是 **密度(Density)** 和 **速度(Velocity)** ,相关的输入配置参数主要位于 **NiagaraComponent** 的`Live Generic`分类下,Fluid Ninja的输入来源可以是以下几种: + +- 静态纹理 +- 动态材质 +- 场景摄像机捕获 +- 渲染目标(RenderTarget) +- 流媒体视频 +- 鼠标输入 +- 场景交互:静态网格体、动态网格体、骨骼网格体、可破坏物... + +> 以上输入并不是独立的,它们可以共存,FluidNinja会将多个输入混合到一起,详见`Help.uasset`中的流程图。 + +在地图`Content/FluidNinjaLive/Tutorial/Levels/NinjaLive_Level03_SimulationInputs_LIVE17.umap`中,有完整的NinjaFluid输入示例,下面对这些输入的关键步骤进行记录: + +#### 静态纹理(Texture) + +静态纹理的输入设置位于NinjaPreset数据表中: + +![image-20231114201238339](Resources/image-20231114201238339.png) + +通常情况下,我们不会直接修改数据表的内容,而是在 **测试关卡** 中,借助 **NinjaLive_PresetManager** 提供的编辑器面板来导出预设表格: + +![image-20231114202724330](Resources/image-20231114202724330.png) + +#### 动态材质(Mateiral) + +材质的输入配置位于 **NinjaComponent** 的 `Live Generic` 分类下: + +![image-20231114202905246](Resources/image-20231114202905246.png) + +- 可以在 **Input Materials** 参数中添加可用的输入材质。可以多添加,以便可以在 **NinjaLive_PresetManager** 中能够切换预览。 +- 调节 **Input Material Selected** 索引选择当前使用的输入材质。 +- 勾选 **RGB-Input Material** 会让材质的RG通道作为Velocity纹理,B通道作为Density纹理,否则 **默认只会使用材质的R通道作为Density纹理** +- 勾选 **Input Material Clamp** ,将会裁剪UV坐标位于[0,1]之外的图像。 + +对于Ninja Fluid的输入材质,需要选择`材质域`为`后期处理`,并关闭`投影光线检测阴影`: + +![image-20231115095919654](Resources/image-20231115095919654.png) + +#### 场景摄像机捕获(Scene Capture 2D) + +FluidNinja可以使用摄像机来捕获场景,并将相机画面内的图像作为流体模拟的输入,具体的配置位于 **NinjaComponent** 的 `Live Generic` 分类下: + +![image-20231115100308055](Resources/image-20231115100308055.png) + +- 可以在 **Input Scene Capture Camera** 选择当前场景已存在的Actor + +示例: + +- 创建一个 **Ninja Live** , **Cube** 和 **场景捕获2D(Scene Capture 2D)** ,并将相机对准要捕获的Cube + + ![image-20231115101605904](Resources/image-20231115101605904.png) + +- 在 **Ninja Live** 的 **Ninja Component** 中,配置场景捕获2D,并选择较为直观的材质输出: + + ![image-20231115101956749](Resources/image-20231115101956749.png) + + ![image-20231115102115854](Resources/image-20231115102115854.png) + + - 执行PIE,能看到如下效果: + + ![image-20231115102315733](Resources/image-20231115102315733.png) + + - 可以在场景捕获2D去调整捕获的配置(相机的参数,对象的筛选...),借助 **NinjaLive_PresetManager** 来调整它的参数效果: + + ![image-20231115103426806](Resources/image-20231115103426806.png) + +#### 渲染目标(RenderTarget) + +材质的输入配置位于 **NinjaComponent** 的 `Live Generic` 分类下: + +![image-20231115103750131](Resources/image-20231115103750131.png) + +- 勾选 **Use Render Target as Input** ,将会使用下方的 **Input Render Target** 作为流体模拟的输入 +- **Input Render Target** 用于指定读取的 Render Target + +在Fluid Ninja中,该方案可以用于制作 **形态可控的3D流体** + +Level3中有如下示例: + +![123123176812](Resources/123123176812.gif) + +- 左侧是一个Niagara粒子系统,中间是投影的RT,右侧是利用RT+输出材质最终展现出的效果 + +示例: + +- 在内容浏览器新建【Niagara粒子系统】,选择【从现有系统拷贝】 + + ![image-20231115104955903](Resources/image-20231115104955903.png) + +- 取消勾选`仅库`,在`标准`分类中找到 **NS_ParticleCaptureBasic** : + + ![image-20231115105156054](Resources/image-20231115105156054.png) + +- 创建完成之后打开粒子系统,可以发现它包含两个默认发射器: + + - **NE_ParticleCaptureBasic** :用于将Empty发射器的粒子,通过网格坐标映射到RenderTarget上,可在AttributeReader中选择捕获哪个发射器。 + - **Empty** :用于模拟的粒子发射器 + + ![image-20231115105833878](Resources/image-20231115105833878.png) + +- 下面给了它一个发射速度(60),并增加了一个四散的随机初速度,可以看到如下效果: + + ![image-20231115110654951](Resources/image-20231115110654951.png) + +- **NE_ParticleCaptureBasic** 会利用捕获的粒子数据,根据所有粒子的坐标 **“投影”** 到一张RenderTarget上,我们可以在用户参数上设置这个投影过程的策略: + + ![image-20231115111735046](Resources/image-20231115111735046.png) + +- 用户参数中,默认使用`RT_PrevivewParticleCapture`作为RT输出,它可能被其他粒子系统共用,直接用它会与其他逻辑(教程关卡)冲突,所以我们需要自己创建一张RT: + + ![image-20231115112113233](Resources/image-20231115112113233.png) + +- 并在NiagaraSystem的默认参数中指定它,尝试调整用户参数来理解RT的映射规则: + + ![image-20231115112418360](Resources/image-20231115112418360.png) + +- 将配置好的NiagaraSystem拖拽放入场景中,并场景中浮空放置一个 NinjaLiveActor ,在 NinjaLive的细节面板中设置`TraceMeshSize`为(5,5,1),在NinjaLiveComponent的细节并开启`追踪网格面向相机(Camera Facing Trace Mesh)`,开启RT输入,并指定刚刚创建的RT,选择输出材质为 `MI_DensityBuffer_Red`: + + ![image-20231115111345866](Resources/image-20231115111345866.png) + + ![image-20231115112955341](Resources/image-20231115112955341.png) + +- 启动PIE,我们能看到如下效果: + + ![image-20231115113119585](Resources/image-20231115113119585.png) + +- 实际上,我们并不需要对NiagaraSystem进行渲染,我们只需要它的模拟数据, **NS_ParticleCaptureBasic** 默认开启只是为了方便调试,所以我们可以关闭粒子发射器的渲染器: + + ![image-20231115113520770](Resources/image-20231115113520770.png) + +- 此外,由于RT的渲染也是依赖相机位置,所以为了获得更准确的平面图像,需要将 **NiagaraSystem** 和 **NinjaLive** 的位置重叠,选择一个合适的输出材质效果,就能看到: + + ![123176812](Resources/123176812.gif) + +#### 视频输入 + +使用视频文件作为NinjaFluid的输入,需要指定以下三个参数: + +![image-20231115181025276](Resources/image-20231115181025276.png) + +- **Input Media Player** :视频播放器 +- **Media Texture** :视频绘制所使用的纹理 +- **Input Media Source** :视频播放源 + +关于UE如何播放视频,可参阅文章 https://zhuanlan.zhihu.com/p/135963353 + +示例: + +- 添加一个NinjaLiveActor,指定视频作为Density输入,再设置一个向上的Velocity贴图`T_Crumple`,结合输出材质`MI_Candle1`,能得到这样的效果: + +![123812](Resources/123812.gif) + +#### 用户输入 + +NinjaLiveActor默认开启了鼠标输入,它的效果如下: + +![1236572](Resources/1236572.gif) + +可以在此处进行关闭: + +![image-20231115182237273](Resources/image-20231115182237273.png) + +#### 碰撞交互 + +Fluid Ninja可以通过物理碰撞来作为输入,来完成一些可交互的流体效果,一个简单的示例如下: + +- 添加一个NinjaLiveActor + + - 设置`Trace Mesh Size`和`Interaction Volume Size`为`(5,5,1)` + - 设置NinjaLiveComponent\Live Bursh Settings,否则会因为碰撞画笔太小导致看不到结果 + - 勾选`Brush Scaled By Interacting Obj Size` + - 设置`Primitive Obj Brush Scale` 为 20 + +- 添加一个小白人,能看到如下效果: + + ![image-20231117163147270](Resources/image-20231117163147270.png) + +该效果是通过UE的物理碰撞机制实现的,其完整的参数均位于 `NinjaLiveComponent\Live Interaction` 中: + +![image-20231117162047588](Resources/image-20231117162047588.png) + +- **Continuous Interaction with Owner Actor** :与持有该组件的Actor持续交互 +- **Continuous Interaction incluisve Obj Type** :过滤交互的碰撞对象类型 +- **Continuous Interaction Component Name** :过滤交互的组件名 +- **Continuous Interaction Bone Names Exact** :过滤交互的骨骼名 +- **Single Target Mode** :单一目标模式,开启之后FluidNinja认为碰撞产生的图像都是由一个目标生成的,这可以使碰撞路径变得平滑,没有断层(插值) +- **Single Target Type** +- **Single Target Mode Skeletal Mesh Index** +- **Single Target Move Set Sim Speed** +- **Speed Influence Factor** :速度影响因子 +- **Clamp Max Velocity** :截断最大速度 +- **Camera Facing Tace Mesh** :TraceMesh始终面向相机 +- **Camera Facing Lock Y-Axis** :锁定摄像机的Y轴 +- **Use Legacy Camera Facing** +- **Trance Mesh Translucent Sort Prio** +- **Use Custom Trace Source** :使用自定义追踪资源,开启之后相当于使用下面的位置作为摄像机位置来进行碰撞检测 +- **Custom Trace Source Position** :自动追踪资源的位置 +- **Trace Mesh Moving in World Space** :TraceMesh位于世界空间 +- **Movement Not Quantized to Steps OnAxis** +- **Movements is Locked on This axis** :锁定某个轴的移动 +- **Force Trace Mesh to Custom Vertical Pos** +- **Force Trace Mesh Vertical Position** :锁定TraceMesh的位置 +- **Enable Paint Buffer Offset in World Spacing** :开启绘制缓存区的世界空间机制 +- **Simple Painter Mode** :单纯的绘制模式,没有碰撞输入 + +对于 **NinjaLiveActor** ,对上述参数做了进一步封装,有如下参数可以调整: + +![image-20231117162207129](Resources/image-20231117162207129.png) + +- **Overlap Based Interaction** :开启碰撞交互 + +- **Interaction Volume Size** :交互体积大小 + +- 以下参数用于 **筛选** 哪些物体可以于交互体积发生“作用”。 + - **Track Actor Primitive Components With Tag** :追踪带有该标签的组件 + + - **Track Actor Skeletal Mesh Components With Tag** :追踪带有该标签的骨骼网格体组件 + + - **Overlap Filter Inclusive Obj Tab** :需要包含的对象碰撞类型 + + - **Overlap Filter Inclusive Bone Name Exact** :需要包含的骨骼名称,如果为空,则会处理全部骨骼 + + - **Overlap Filter Inclusive Bone Name Partial** :需要包含的骨骼名称字段,例如填入Foot,则所有名称包含Foot的骨骼都将被包含 + + - **Exclude Specific Actors from Overlap** :需要排除的Actor类型 + + - **Auto Exclude Large Overlapping Objects** :自动排除大型的覆盖对象 + +### 调节画刷 + +画刷用于控制FluidNinja输入贴图作用区域的缩放因子,其参数配置如下: + +![image-20231117161748514](Resources/image-20231117161748514.png) + +一些关键参数: + +- **Brush Scaled Inversely by Trace Mesh Size** :画刷会随跟踪网格的大小进行缩放 +- **Brush Scaled by Interacting Obj Size** :画刷会随交互对象的大小进行缩放 +- **Use Obj Bounds Instead of Size** :使用交互对象的边界作为画刷尺寸 +- **Global Brush Scale** :全局画刷缩放系数 + +> 如果碰撞交互的效果几乎不可见,很有可能是画刷太小导致的 + +### 预设配置 + +预设表包含了流体模拟相关的参数: + +![image-20231117165456545](Resources/image-20231117165456545.png) + +在 **NinjaLive_PresetManager** 阅读参数的提示信息,通过调整来理解它跟效果的关联关系,配完参数后另存为新表。 + +### 输出材质 + +FluidNinjaLive内容目录下定义了其使用的核心材质: + +![image-20231117161325291](Resources/image-20231117161325291.png) + +我们可以直接使用`Instance_*`文件夹下的材质实例 + +![image-20231117161142891](Resources/image-20231117161142891.png) + +亦或是新建材质实例,设置父项为FluidNinja提供的基础材质(材质文件名称以`M_NinjaOutput_`开头): + +![image-20231117161054031](Resources/image-20231117161054031.png) + +- **M_NinjaOutput_Basic** :基础材质 +- **M_NinjaOutput_Advanced** :高级材质 +- **M_NinjaOutput_TranslucentReflective** :透明反射材质 +- **M_NinjaOutput_WorldSpaceGeneric** :世界空间材质 +- ... + +以M_NinjaOutput_Basic为例,它提供了很多可供调整的参数预设: + +![image-20231117161508777](Resources/image-20231117161508777.png) + +在FluidNinja的Tutorial和Usercase关卡中,有很多可供参考的材质示例,具体的参数意义,可通过复刻进行学习。 + +## 总结 + +FluidNinja的思路很巧妙,使用起来有些繁琐,但关键的步骤很清晰: + +- 构造模拟过程所需的Density(密度)贴图和Velocity(速度)贴图,使用恰当的画刷和预设并结合对应的材质来呈现出最终想要的效果。 + +### 优缺点 + +- FluidNinja通过二维面片来伪装出三维视觉的效果,它的性能损耗远低于常规的粒子流体模拟,但想要伪装出足够真实的三维效果,就要有足够真实的二维输入,否则的话,在某些视角上可以轻易识破FluidNinja的追踪网格面片。 +- FluidNinja提供了很多的方式构建流体的输入:贴图,材质,RT,Niagara,相机...,可以满足绝大部分的开发需求,但细节很多,使用起来并不容易,想要随心所欲的制作出想要的效果还需要深入的学习。 +- FluidNinja并不能完全代替粒子,它更适用于 **可交互的** 流体模拟,比如烟雾,火焰,水体... diff --git "a/Docs/04-UnrealEngine/6.\345\274\200\346\224\276\344\270\226\347\225\214\345\210\266\344\275\234.md" "b/Docs/04-UnrealEngine/6.\345\274\200\346\224\276\344\270\226\347\225\214\345\210\266\344\275\234.md" new file mode 100644 index 00000000..40993cfa --- /dev/null +++ "b/Docs/04-UnrealEngine/6.\345\274\200\346\224\276\344\270\226\347\225\214\345\210\266\344\275\234.md" @@ -0,0 +1,724 @@ +--- +comments: true +--- + +# Unreal Engine 5 开放世界 + +笔者之前整理过一个非技术向的开放世界杂谈: + +- [游戏开发杂谈-开放世界 - 知乎 / Italink](https://zhuanlan.zhihu.com/p/648572742) + +这篇文章会更详细地说明使用UE5制作开放世界时需要注意什么,以及将会面临什么。 + +## 世界分区 + +在UE5中制作开放世界地图,要明确 **分区(Partition)** 和 **流送(Streaming)** 的概念,这里有一些 **必看! 必看! 必看!** 的内容: + +- [在虚幻引擎中构建虚拟世界 | 虚幻引擎5.3文档 (unrealengine.com)](https://docs.unrealengine.com/5.3/zh-CN/building-virtual-worlds-in-unreal-engine/) +- [Unreal Fest 2023 - Building Bigger](https://www.bilibili.com/video/BV1UC4y1V7jt) +- [[UFSH2023\]从创建小场景到构建大世界的工作流升级 | Chris Murphy Epic Games](https://www.bilibili.com/video/BV1GK4y1676a/) + +还有一些通俗易懂的讲解视频: + +- [【UE地形】世界分区(World Partition)是什么?怎么用?- bilibili](https://www.bilibili.com/video/BV1ik4y1A77p/) +- [UE5 World Partition Part 1 Concepts - YouTube](https://www.youtube.com/watch?v=fDgKV3SjDTI) + +简而言之,就是UE将地图划分成很多个 **单元格(Cell)** ,随着 **玩家(流送源)** 的移动去 **流送(Streaming)** 周围的单元: + +![image-20231103161359654](Resources/image-20231103161359654.png) + +在上图中,我们看到的只是一个四向的划分,但事实上,UE的世界分区采用了一种 **稀疏四叉树** 的方式进行流送,它还具有多个网格级别(Level),我们看到的只是Level0: + +![image-20231103164631165](Resources/image-20231103164631165.png) + +### 分区 + +世界分区会根据 **物体的包围盒** 来确定它归属于哪个 **网格级别(Grid Level)** 的 **单元格(Cell)** 。 + +打个比方,假如我们世界分区域中存在如下物体,红色区域是它们 **包围盒(Bound)** 的 **水平面积(Area)** : + +![image-20231128100717690](Resources/image-20231128100717690.png) + +世界分区在划分时,会从最低的网格级别开始,将物体划分到刚好能囊括其面积的单元格之中 + +因此上述物体分布的最终划分情况如下: + +![image-20231128100134840](Resources/image-20231128100134840.png) + +在此处可以对分区相关的参数进行设置: + +![image-20231130100005205](Resources/image-20231130100005205.png) + +勾选 **预览网格** 能在编辑器视图下,看到场景的流送视图: + +![image-20231130101753095](Resources/image-20231130101753095.png) + +### 流送 + +场景中新建的Actor默认都会参与流送,也就是会被卸载,在Actor的细节面板上可以做相关调整: + +![image-20231201112501518](Resources/image-20231201112501518.png) + +- **运行时网格** :该Actor归属于哪个世界分区网格,如果为None会被指定到 **MainGrid** 中。 +- **已空间加载** :勾选后该Actor会参与世界分区的流送,取消勾选会让该Actor在场景中永远被加载,不参加流送。 + +世界分区的流送由 **流送源(Streaming Source)** 来触发。 + +流送源对应的C++接口为: **IWorldPartitionStreamingSourceProvider** + +![image-20231128110811971](Resources/image-20231128110811971.png) + +**玩家控制器(PlayerController)** 是一个默认的流送源: + +![image-20231128111620135](Resources/image-20231128111620135.png) + +假如此时人物位于地图的右下角,结合上面的划分情况,整体加载情况可能是这样的: + +![image-20231128101425779](Resources/image-20231128101425779.png) + +我们也可以通过 **世界分区流送源组件(WorldPartitionStreamingSourceComponent)** 来让其他actor也能作为流送源: + +![image-20231128111308024](Resources/image-20231128111308024.png) + +![image-20231128111209378](Resources/image-20231128111209378.png) + +#### 调试视图 + +官方提供了一些指令可以预览流送的过程: + +- `wp.Runtime.ToggleDrawRuntimeHash2D` + + ![image-20231127182325974](Resources/image-20231127182325974.png) + +- `wp.Runtime.ToggleDrawRuntimeHash3D`![image-20231127182512039](Resources/image-20231127182512039.png) + +- 在开启预览视图后: + + - 使用指令 `wp.Runtime.ShowRuntimeSpatialHashGridLevel ${LevelIndex}`来调整预览的层级。 + + - 使用指令 `wp.Runtime.ToggleDrawRuntimeCellsDetails` 可以看到单元格加载的耗时: + + ![image-20231129134856514](Resources/image-20231129134856514.png) + + - 使用指令 `wp.Runtime.ShowRuntimeSpatialHashCellStreamingPriority 1`查看单元格流送的优先级热图 + + ![image-20231129135711872](Resources/image-20231129135711872.png) + +#### 关键配置 + +##### 联机 + +在多人联机时,服务器默认不会启用流送,场景流送只是玩家的本地行为: + +![image-20231128134921374](Resources/image-20231128134921374.png) + +使用指令`wp.Runtime.EnableServerStreaming 1`可以开启服务器上的流送 + +![image-20231128135313851](Resources/image-20231128135313851.png) + +##### 流送优化 + +世界分区默认是开启流送优化的,并非每一帧都会更新流送的状态,通过以下配置我们可以调整优化的策略: + +- `wp.Runtime.UpdateStreaming.EnableOptimization true`:默认为true,开启流送优化 +- `wp.Runtime.UpdateStreaming.ForceUpdateFrameCount 0`:默认为0,可以设置固定每多少帧去更新流送状态 +- `wp.Runtime.UpdateStreaming.LocationQuantization 400`:默认为400,当移动距离大于400时触发流送状态的更新 +- `wp.Runtime.UpdateStreaming.RotationQuantization 10`:默认为10,当旋转角度大于10时触发流送状态的更新 + +### 代码流程 + +对于开发者来说,深入了解世界分区的流程对整体项目的把控至关重要!美术人员可跳过。 + +这里挑出一些主要的步骤进行说明。 + +UE世界分区的核心结构为: **UWorldPartition** + +它存放在 **AWorldSettings** 中,可以通过如下方法来获取到它: + +``` c++ +UWorldPartition* UWorld::GetWorldPartition() const +{ + AWorldSettings* WorldSettings = GetWorldSettings(/*bCheckStreamingPersistent*/false, /*bChecked*/false); + return WorldSettings ? WorldSettings->GetWorldPartition() : nullptr; +} +``` + +Actor对世界分区的支持主要体现在,Actor基类中添加了一个 **编辑器下** 的虚函数,用于创建世界分区的Actor描述: + +``` c++ +class AActor: public UObject +#if WITH_EDITOR + virtual TUniquePtr CreateClassActorDesc() const; +#endif +} +``` + +它可以由子类复写,来填充这样的结构: + +``` c++ +class FWorldPartitionActorDesc +{ + // Persistent + FGuid Guid; + FTopLevelAssetPath BaseClass; + FTopLevelAssetPath NativeClass; + FName ActorPackage; + FSoftObjectPath ActorPath; + FName ActorLabel; + FVector BoundsLocation; + FVector BoundsExtent; + FName RuntimeGrid; + bool bIsSpatiallyLoaded; + bool bActorIsEditorOnly; + bool bActorIsRuntimeOnly; + bool bActorIsHLODRelevant; + bool bIsUsingDataLayerAsset; // Used to know if DataLayers array represents DataLayers Asset paths or the FNames of the deprecated version of Data Layers + FName HLODLayer; + TArray DataLayers; + TArray References; + TArray Tags; + FPropertyPairsMap Properties; + FName FolderPath; + FGuid FolderGuid; + FGuid ParentActor; // Used to validate settings against parent (to warn on layer/placement compatibility issues) + FGuid ContentBundleGuid; + + // Transient + mutable uint32 SoftRefCount; + mutable uint32 HardRefCount; + UClass* ActorNativeClass; + mutable TWeakObjectPtr ActorPtr; + UActorDescContainer* Container; + TArray DataLayerInstanceNames; + bool bIsForcedNonSpatiallyLoaded; +}; +``` + +引擎之中有不少覆写: + +![image-20231017112528040](Resources/image-20231017112528040.png) + +在编辑器下,世界分区地图中新建Actor,会自动创建 **FWorldPartitionActorDesc** : + +![image-20231017113022901](Resources/image-20231017113022901.png) + +在编辑器中,所有创建的ActorDesc存储在 **UWorldPartition** 的`ActorDescContainer`中: + +``` c++ +class UWorldPartition final + : public UObject + , public FActorDescContainerCollection + , public IWorldPartitionCookPackageGenerator +{ + + UPROPERTY(Transient) + TObjectPtr ActorDescContainer; //存储ActorDesc的容器,也被称为**MainContainer** + + UPROPERTY() + TObjectPtr RuntimeHash; //用于定制Actor的分区划分策略 + + UPROPERTY(Transient) + TObjectPtr World; +}; +``` + +虽然`ActorDescContainer`是临时变量,不参与序列化,但会在打包时进行存储: + +``` c++ +bool UWorldPartition::GatherPackagesToCook(IWorldPartitionCookPackageContext& CookContext) +{ + TArray PackagesToCook; + if (GenerateContainerStreaming(ActorDescContainer, &PackagesToCook)) + { + FString PackageName = GetPackage()->GetName(); + for (const FString& PackageToCook : PackagesToCook) + { + CookContext.AddLevelStreamingPackageToGenerate(this, PackageName, PackageToCook); + } + return true; + } + return false; +} +``` + +#### 分区 + +其中 **UWorldPartition** 的Actor分区划分策略由 `RuntimeHash` 实现,在编辑器下,`RuntimeHash`会在每次启动地图时都会重新进行划分,而在打包时,也会将划分的数据进行烘培: + +``` c++ +bool UWorldPartition::PrepareGeneratorPackageForCook(IWorldPartitionCookPackageContext& CookContext, TArray& OutModifiedPackages) +{ + check(RuntimeHash); + return RuntimeHash->PrepareGeneratorPackageForCook(OutModifiedPackages); +} + +bool UWorldPartition::PopulateGeneratorPackageForCook(IWorldPartitionCookPackageContext& CookContext, const TArray& InPackagesToCook, TArray& OutModifiedPackages) +{ + check(RuntimeHash); + return RuntimeHash->PopulateGeneratorPackageForCook(InPackagesToCook, OutModifiedPackages); +} + +bool UWorldPartition::PopulateGeneratedPackageForCook(IWorldPartitionCookPackageContext& CookContext, const FWorldPartitionCookPackage& InPackagesToCook, TArray& OutModifiedPackages) +{ + check(RuntimeHash); + return RuntimeHash->PopulateGeneratedPackageForCook(InPackagesToCook, OutModifiedPackages); +} + +UWorldPartitionRuntimeCell* UWorldPartition::GetCellForPackage(const FWorldPartitionCookPackage& PackageToCook) const +{ + check(RuntimeHash); + return RuntimeHash->GetCellForPackage(PackageToCook); +} +``` + +在编辑器下,启动开放世界地图时,会调用`UWorldPartition::OnBeginPlay`: + +``` c++ +void UWorldPartition::OnBeginPlay() +{ + TArray OutGeneratedStreamingPackageNames; + GenerateStreaming((bIsPIE || IsRunningGame()) ? &OutGeneratedStreamingPackageNames : nullptr); + + // Prepare GeneratedStreamingPackages + check(GeneratedStreamingPackageNames.IsEmpty()); + for (const FString& PackageName : OutGeneratedStreamingPackageNames) + { + // Set as memory package to avoid wasting time in UWorldPartition::IsValidPackageName (GenerateStreaming for PIE runs on the editor world) + FString Package = FPaths::RemoveDuplicateSlashes(FPackageName::IsMemoryPackage(PackageName) ? PackageName : TEXT("/Memory/") + PackageName); + GeneratedStreamingPackageNames.Add(Package); + } + + RuntimeHash->OnBeginPlay(); +} +``` + +其中`GenerateStreaming`函数会进一步调用到`UWorldPartition::GenerateContainerStreaming`,在该函数中,会从 **MainContainter** 中 **递归搜索出所有的Containter和ActorDesc** + +有些ActorDesc会持有自己的Containter,就比如 **FLevelInstanceActorDesc** 就是一个特例,它是放置在场景中的关卡实例,其中主要的代码定义如下: + +``` c++ +class FLevelInstanceActorDesc : public FWorldPartitionActorDesc +{ +public: + FLevelInstanceActorDesc(); + virtual ~FLevelInstanceActorDesc() override; + + virtual bool IsContainerInstance() const override; + virtual bool GetContainerInstance(const UActorDescContainer*& OutLevelContainer, + FTransform& OutLevelTransform, + EContainerClusterMode& OutClusterMode) const override; +protected: + FTransform LevelInstanceTransform; + ELevelInstanceRuntimeBehavior DesiredRuntimeBehavior; + TWeakObjectPtr LevelInstanceContainer; //关卡实例的ActorDescContai +private: + void RegisterContainerInstance(UWorld* InWorld); + void UnregisterContainerInstance(); +}; +``` + +在搜集到所有的Containter和ActorDesc之后,将会调用: + +``` c++ +bool UWorldPartitionRuntimeSpatialHash::GenerateStreaming(UWorldPartitionStreamingPolicy* StreamingPolicy, + const IStreamingGenerationContext* StreamingGenerationContext, + TArray* OutPackagesToGenerate); +``` + +该函数主体流程是: + +- 搜集场景中所有的网格配置(不只是WorldSetting中含有网格配置,一些Actor也会提供,比如 **ASpatialHashRuntimeGridInfo** ,它目前主要用于HLOD) + +- 遍历所有的ActorDesc,依据ActorDesc中的 `运行时网格(RuntmeGrid)` 配置 来将其划分到对应 Grid 的 **FActorSetInstance** 中 + +- 对于每个Grid的 **FActorSetInstance** ,将会调用: + + ``` c++ + FSquare2DGridHelper GetPartitionedActors(const FBox& WorldBounds, + const FSpatialHashRuntimeGrid& Grid, + const TArray& ActorSetInstances) + ``` + + 来将该Grid中的所有Actor划分到具体的 **单元(Cell)** 中 + +#### 流送 + +流送的逻辑主要位于: + +![image-20231129161940907](Resources/image-20231129161940907.png) + +### 关键问题 + +世界分区(World Partition)可以让我们轻易实现无缝加载的大世界,但流送的特性也带来了很多限制,这往往需要开发商具有更加成熟的制作团队,并配备专业的引擎团队,在设计时充分考虑到世界分区的特性,并且 **需要专业人员来监管世界分区的流送策略** 。 + +#### 跨边界 + +观察之前的物体划分和流送情况: + +![image-20231129094519448](Resources/image-20231129094519448.png) + +![image-20231128203302065](Resources/image-20231128203302065.png) + +可以看到一些 “奇怪” 的现象, + +- 由于 物体`E` 的包围盒位于世界分区的轴线上,结果被划分到了Level3,这将导致它会永远被加载,但是它的包围盒区域明明非常小。 + +这个问题非常容易出现: + +![image-20231128102516857](Resources/image-20231128102516857.png) + +##### 解决方案 + +引擎提供了一些指令有助于缓解该现象: + +- `wp.Runtime.RuntimeSpatialHashUseAlignedGridLevels`:是否开启网格级别的向上对齐 +- `wp.Runtime.RuntimeSpatialHashSnapNonAlignedGridLevelsToLowerLevels`:将未对齐的物体划分到更低的网格级别 +- `wp.Runtime.RuntimeSpatialHashPlaceSmallActorsUsingLocation `:包围盒小于单元格尺寸的物体将直接使用Location进行划分。 +- `wp.Runtime.RuntimeSpatialHashPlacePartitionActorsUsingLocation`:包围盒小于单元格尺寸的分区物体(植被,水体和地形)将直接使用Location进行划分。 + +此处有关于它的描述: + +- [Tech Note: World Partition Spatially Loaded Actors are Always Loaded - Epic Developer Community Forums ](https://forums.unrealengine.com/t/tech-note-world-partition-spatially-loaded-actors-are-always-loaded/661466) + +但这并不是一个很好的解决方案,为了能规避这些问题,UE允许开发团队根据自己的场景需求定制 **RuntimeHashClass** ,在此处进行替换: + +![image-20231017150541788](Resources/image-20231017150541788.png) + +> 可参考:`Engine\Source\Runtime\Engine\Private\WorldPartition\RuntimeSpatialHash\RuntimeSpatialHashGridHelper.cpp` + +在UE5.3中,官方把上述配置公开到了编辑器中,并允许调整世界分区的坐标原点,它能满足绝大部分的需求: + +![image-20231128103524123](Resources/image-20231128103524123.png) + +#### 加载优先级 + +因为世界分区流送的缘故,将导致场景中的一些运动物体出现异常,重力是其中最常见的: + +![1232265272](Resources/1232265272.gif) + +上图中,存在的问题是: + +- 部分立方体被划分到其中一个单元格中,由于划分算法的局限性,地面被划分到了另一个单元格中,由于地面所在的单元格不在加载范围之内,这将导致物体直接掉落到地底 + +还有其他情况也会触发这个问题: + +- 世界分区的流送是 **逐单元(Cell)** 进行的,而 **不是逐Actor** ,当一个单元位于流送源的加载范围之内,会 **异步** 地 **加载(Loading** ) 该单元的所有Actor,这个加载顺序是无法保证的,如果先加载了立方体,再加载地面,这期间如果执行了物理Tick,物体也会掉到地面。(这在场景压力比较大时更容易出现) + +##### 解决方案 + +解决这个问题的主要思路是: + +- 让 重力承载物体 所在的世界分区网格具有更大的加载范围。 +- 把 重力物体 移到比 重力承载物体 加载优先级更低的世界分区网格中。 + +#### 数据持久性 + +参与世界分区的Actor并不具有数据持久性,当我们对一个加载之后的物体进行了修改,卸载时,修改的数据并不会保存。 + +比如这个例子:界分区中有一群浮空的立方体,加载之后,因为重力,它会掉向地面,卸载之后再加载,可以发现立方体已经正确的保存了之前的状态,落在地面上: + +![123d23422](Resources/123d23422.gif) + +世但当我们开启控制台指令 `gc.ForceCollectGarbageEveryFrame 1`,再来看: + +![123asd23422](Resources/123asd23422.gif) + +这时重新加载立方体,立方体会回到起始位置。 + +第一个示例让我们产生 物体修改能被保存 的错觉,是因为超出加载范围的单元格不会立即被释放,而是处于 `Unloaded Still Around` 状态,这样能保证下次加载的时候可以立即复用,真正卸载单元格的时机是在下次GC。 + +![image-20231129112140082](Resources/image-20231129112140082.png) + +##### 解决方案 + +这个问题是由流送的特性导致: + +- 流入时会加载资产到内存,流出时会将资产卸载(销毁资源),而不会保存资产的修改。 + +那是否意味着我们需要在流出时保存资产的修改? + +- 很明显,不需要,除非你想进行永久性的修改,即重新打开游戏之后,之前的修改还在,且无法还原。 + +通常情况下,这种修改只会维持在一定作用域(生命周期)之内,比如重进游戏,重进关卡,重开存档之后,这种修改就应该丢失。 + +只不过现在我们想要的是,流送不会导致数据的丢失。 + +那其实只需要: + +- 让这部分数据不要参与流送,即数据存储在一个合理的作用域,而不是存到Actor中。 + +UE5.3中提供了一个实验性插件 **LevelStreamingPersistence** 可以帮助开发者解决该问题。 + +![image-20231204160944855](Resources/image-20231204160944855.png) + +该插件通过 **ULevelStreamingPersistenceSettings** 来配置引擎中哪些属性需要持久性: + +![image-20231211095416739](Resources/image-20231211095416739.png) + +在世界分区流送单元时,会根据以上配置将流出的单元中具有持久性的属性进行保存,对于流入的单元,使用之前的历史值赋值: + +要解决上面的重力问题,只需要我们在`DefaultEngine.ini`中,增加如下配置: + +``` c++ +[/Script/LevelStreamingPersistence.LevelStreamingPersistenceSettings] +Properties=(Path="/Script/Engine.SceneComponent:RelativeLocation",bIsPublic=False) +Properties=(Path="/Script/Engine.SceneComponent:RelativeRotation",bIsPublic=False) +``` + +通过 **PropertyPath** 来确定哪些属性往往会得到非常宽泛的范围,比如上面的配置将序列化所有场景组件的位置和旋转,而并非所有场景组件都需要持久化,该插件也提供了一些委托用于进一步筛选哪些对象需要序列化: + +![image-20231211100646090](Resources/image-20231211100646090.png) + +其中 `bIsPublic` 决定了该属性被保存的值是否可以被手动修改,而不仅仅是根据流送自动序列化,其中操作的接口主要位于 **ULevelStreamingPersistenceManager** 中: + +``` c++ +class ULevelStreamingPersistenceManager : public UWorldSubsystem +{ + GENERATED_BODY() + + // Serialization + bool SerializeTo(TArray& OutPayload); + bool InitializeFrom(const TArray& InPayload); + + // Sets property value and creates the entry if necessary, returns true on success. + template + bool SetPropertyValue(const FString& InObjectPathName, const FName InPropertyName, const PropertyType& InPropertyValue); + + // Sets the property value on existing entries, returns true on success. + template + bool TrySetPropertyValue(const FString& InObjectPathName, const FName InPropertyName, const PropertyType& InPropertyValue); + + // Gets the property value if found, returns true on success. + template + bool GetPropertyValue(const FString& InObjectPathName, const FName InPropertyName, PropertyType& OutPropertyValue) const; + + // Sets the property value converted from the provided string value on existing entries, returns true on success. + bool TrySetPropertyValueFromString(const FString& InObjectPathName, const FName InPropertyName, const FString& InPropertyValue); + + // Gets the property value and converts it to a string if found, returns true on success. + bool GetPropertyValueAsString(const FString& InObjectPathName, const FName InPropertyName, FString& OutPropertyValue); +}; +``` + +#### 运行时生成 + +世界分区划分是在编辑器中进行的,运行时生成(Spawn)的任何物体都不会参与世界分区的流送,试想一个这样的情景: + +- 在场景中Spawn了一个具有重力的小球,它落到了地面,当角色远离该区域导致地面被流出时,小球将会掉到地底。 + +这显然不是我们想要的,一般情况下,场景中除角色之外的所有常驻物体都应该在一开始就放置在地图关卡中。 + +但有时候确实存在一些特殊的需求: + +- 一些场景物体可能存在一些运行时的事件依赖,因此必须放在运行时去 生成(Spawn)。 + +##### 解决方案 + +- 如果物体的位置是固定不变的,可以提前在场景中放置一个用于占位的Actor,这样可以使得它在Runtime时是可配置的。 +- 如果物体位于一定区域,可以在该区域放置一个 **触发器体积(Trigger Volume)** ,通过触发器逻辑来控制物体的生命周期,如果触发区域的大小超过世界分区的加载范围,那么应该将触发器体积放置在一个加载优先级更高的网格中,并添加流送源组件。 + +#### 人物传送 + +在世界分区中进行人物传送(传送)时,由于流送需要一定时间,如果将人物直接转移到目标位置,很可能因为周边物体还没有加载完成,角色因为重力直接掉到地底。 + +##### 解决方案 + +针对这个问题,我们需要在目标区域进行预加载,并等待加载完成。 + +- 方案一:关闭角色Tick,将角色传送到目标点,以角色自身作为流送源触发周边区域的流送, **轮询(FTSTicker)** 到目标区域加载完成时开启Tick,在黑客帝国的Mass框架下也有相应的参考: + + ![image-20231129150658814](Resources/image-20231129150658814.png) + +- 方案二:在目标位置Spawn一个带流送源组件的Actor,开启流送,轮询加载是否完成,完成后再将角色传送过去: + + ![image-20231129151231027](Resources/image-20231129151231027.png) + +### One File Per Actor + +OFPA 允许团队成员可以共同编辑同一地图的不同Actor + +传统地图会将 所有Actor 存储在 World 对应的资产包中,而UE的开放世界地图默认启用了 [ **一Actor一文件(One File Per Actor,简称OFPA)** ](https://docs.unrealengine.com/5.3/zh-CN/one-file-per-actor-in-unreal-engine/),即每个Actor都对应一个文件。 + +这些文件存储在项目Content目录下,`__ExternalActors__`和`__ExternalObjects__`文件夹中的对应地图路径下,下图表示了名为`OpenWorld`的地图中的所有Actor: + +![image-20231017111331540](Resources/image-20231017111331540.png) + +我们可以在场景大纲中获取Actor的真实文件路径: + +![image-20231017111622364](Resources/image-20231017111622364.png) + +通常情况下,所有Actor都存储在`__ExternalActors__`中,而`__ExternalObjects__`存储的是一些UObject(非Actor)对象,比如各类Setting,大纲分组等。 + +### HLOD + +通过世界分区的流送,我们可以将全场景的性能压力缩减到只需关注流送源的周边区域: + +![image-20231129151341077](Resources/image-20231129151341077.png) + +对于游戏逻辑而言,这种流送固然是一种优化,但对场景来说,我们并不希望因为流送而卸载掉远处的场景,而HLOD正是为了解决这个问题。 + +**HLOD用于在世界分区中显示远景** ,在开放世界地图中,它的使用方式不同于普通地图(UE4)的HLOD: + +- [虚幻引擎中的世界分区 - 分层细节级别 | 虚幻引擎5.3文档 (unrealengine.com)](https://docs.unrealengine.com/5.3/zh-CN/world-partition---hierarchical-level-of-detail-in-unreal-engine/) + +官方文档中也缺乏HLOD相应的细节描述,对于不了解它定位的小伙伴,很容易滥用HLOD,在世界分区中使用HLOD的本意是: + +- 将 **单元格(Cell)** 中 **所有Actor** 合并为一个低精度的 单元格模型代理,当单元格超出流送源的加载范围时,可以使用该简模显示,这样做,在显示远景的同时,即减少了渲染消耗(使用低精度模型和材质),又优化了遮挡剔除的计算量(减少了Actor的数量) + + ![image-20231129152538095](Resources/image-20231129152538095.png) + + ![image-20231129153201753](Resources/image-20231129153201753.png) + +我们可以在此处设置整个地图的 默认(缺省)HLOD层 配置,它会对所有Actor生效: + +![image-20231027163751104](Resources/image-20231027163751104.png) + +它的资产配置面板如下: + +![image-20231027164043816](Resources/image-20231027164043816.png) + +- **层类型:** 使用何种策略来生成HLOD + - **实例化** :使用对应模型最低级别的LOD(如果是Nanite模型则会使用该LOD生成新的Nanite数据)。 + - **合并** :将分区内的静态网格体合并到一起。 + - **简化** :对分区内的网格体进行合并,在合并网格体的基础上执行网格体简化,它更适合几何形状比较复杂的物体,比如枝繁叶茂的树木。 + - **近似** :对分区内的网格体进行合并,在合并网格体的基础上执行网格体近似,它更适合几何形体比较圆润的物体,比如建筑,石头。 + - **自定义** :自定义HLOD生成策略。 + +- **已空间加载:** 生成的HLOD是否需要开启流送,如果开启,那么会为该HLOD层创建一个 HLOD Grid,生成的HLODActor会放置在这个Grid中。 + - **单元大小:** 配置HLOD Grid的单元大小,数值最好是Main Grid单元大小的整数倍。 + - **加载范围:** 配置HLOD Grid的加载范围,数值最好是Main Grid单元大小的整数倍。 + - **父层:** 对生成的HLOD简模,使用父HLOD层再处理一遍,生成更精简的HLOD简模,通常它具有更大的加载范围。 + +注意事项: + +- 黑客帝国的场景物体多是棱角分明的建筑,它统一使用了 定制的 近似网格 策略,当场景具有更复杂的物体种类时(比如树木,水体,植被),则需要定制更加细化的处理策略,这往往需要非常严格的资产管理策略作为支撑,此外,深入了解HLOD相关代码流程和UE所提供的网格处理算法对开放世界项目的性能优化是非常有价值的。 +- HLOD是将一个网格的单元合并为一个简模,但如果一块区域存在多个网格,那么该区域就会生成多个简模,且不同网格之间的模型不会合并,因此,在通常情况下,所有场景物体都应该该放在MainGrid中,只需要对MainGrid中的物体生成HLOD,对于小型物件,可以放到一个加载范围更小的网格中,不参与HLOD的生成。 + +### 数据层 + +数据层是 UE 5 为开放世界所提供的一项功能,它的用法类似于大纲文件夹: + +![image-20231020115448454](Resources/image-20231020115448454.png) + +![image-20231020113158786](Resources/image-20231020113158786.png) + +但与文件夹不同的是,它还提供了两项关键功能: + +- 可以在编辑器打开地图时,默认不加载该类别的Layer。 +- 可以在运行时,通过相关接口来 加载和卸载 Layer + +为什么需要使用数据层? + +- 可以利用 数据层 将 场景物体 **根据 类别 分层** , **根据 区域 分块** ,通过这样的分类可以 **缓解编辑器全场景加载的压力** ,在多人编辑时,各个人员可选择性地加载自己关注的区域,这在大型场景管理和多人协作中至关重要,如果它没有处理好,那将会是场景制作流程混乱的开端。 +- 编辑器下的分类能够 **给后续的场景优化提供有效的依据** 。 +- 数据层是参与世界分区流送的 **子关卡** 。 + +在此处可以打开数据层面板: + +![image-20231020115915843](Resources/image-20231020115915843.png) + +右键创建 **该地图** 的 **数据层** : + +![image-20231020115943300](Resources/image-20231020115943300.png) + +这样创建 **所有地图可公用** 的 **数据层资产** + +![image-20231020133605969](Resources/image-20231020133605969-1698391657801-4.png) + +可以在此处手动设置Actor隶属于哪个Actor: + +![image-20231027153421929](Resources/image-20231027153421929.png) + +也可以这样: + +![image-20231027153551427](Resources/image-20231027153551427.png) + +我们也可以设置数据层的 **当前上下文(Current Context)** ,新建的Actor会自动添加到当前数据层中: + +![image-20231027153936040](Resources/image-20231027153936040.png) + +![image-20231027154047197](Resources/image-20231027154047197.png) + +按这样来移除或清理当前数据层: + +![image-20231027154204423](Resources/image-20231027154204423.png) + +### 关卡实例 + +UE 5 中提供了 关卡实例化(Level Instancing)的功能,旨在改善和简化世界分区的编辑体验。 + +- [虚幻引擎中的关卡实例化 | 虚幻引擎5.3文档 (unrealengine.com)](https://docs.unrealengine.com/5.3/zh-CN/level-instancing-in-unreal-engine/) + +关卡实例的本意是将一组Actor编组,这样就可以在整个世界内放置和复用的关卡实例,但由于它可以在打包时嵌入到主关卡的世界分区中,再加上目前UE5数据层的使用还未得到普及,很多习惯UE4的开发团队会喜欢将它作为 **开放世界地图的子关卡** 来使用。 + +在5.3中也解决了之前无法为关卡实例中的Actor设置 **运行时网格** 的问题: + +![image-20231129161229035](Resources/image-20231129161229035.png) + +对于关卡实例中的Actor,也多了一个`仅在主世界`的选项,勾选之后,该Actor只有在打开关卡实例地图时才会被加载,当作为关卡实例被其他地图加载时,不会加载此Actor: + +![image-20231129161335413](Resources/image-20231129161335413.png) + +## 场景制作 + +这里有一篇文章罗列了许多内容: + +- [【UE5】目录、资产命名规范 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/484119115) + +这里有一些建议: + +- UE只是引擎技术的供应商,它提供了许多优秀的技术能力和通用编辑器,但并不存在一套标准的场景制作解决方案,这是需要研发团队耗费大量的试错成本去做技术的积累和选型。 + +- 需要明白人的力量是有限的,因此在制作资产时最好尽可能地采用程序化,在管控流程上尽可能使用自动化,寻找到合适的量化指标,来让制作流程变得可追溯,可迭代: + - Houdini和UE5的PCG框架: + - [[UE5 PCG] 官方公开课:深入研究Electric Dreams环境项目](https://link.zhihu.com/?target=https%3A//www.bilibili.com/video/BV1y8411U7Hs) + - [Electric Dreams PCG技术详解(一)——术语、工具和图表介绍](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/mGWiPKvWU_NHTktIWA6LnA) + - [Electric Dreams PCG技术详解(二)——沟壑、大型Assembly](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/xHdnrFTkywF_7OjqOuALbw) + - [Electric Dreams PCG技术详解(三)——森林、岩石和场景背景](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/YWcd8l-VphNLrOlg3zkSzA) + - [Electric Dreams PCG技术详解(四)——远景、雾气、自定义节点及子图表](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/CWTUcRIBTXw8WvhX3jrcBw) + - [虚幻引擎自动化系统 | 虚幻引擎5.3文档 (unrealengine.com)](https://docs.unrealengine.com/5.3/zh-CN/automation-system-in-unreal-engine/) + - [程序化生成和深度学习 | EA工作室](https://www.bilibili.com/video/BV1yh411M7pA) + +- 开放世界场景的性能优化不同于单个资产的优化,因为本身存在着大量需要维护的资产,很难靠人力进行维护,所以应该尽可能从宏观上寻求一些统一的管理策略,并舍弃一些小的优化要求来换取更大的性能平衡,做好 **场景资产的分类和隔离** 是非常非常非常重要的,它是性能优化的重要依据,研发团队可以根据不同的分类来制定相应的LOD Group,Texture Group和HLOD生成策略。此外,对于一些确实需要进行特化处理的资产,也应该保留特化的依据(白名单)。 + +- 场景制作流程不会混乱的前提是每个参与者都严格遵守场景的制作规范,因此需要研发团队建立公共的文档仓库用于公布和迭代场景规范, 尽可能依靠工具而非人工进行校验,这样可以很大程度地减少人为介入产生错误的概率,如果非要增加相关人员的手动工作量,也需要尽可能地评估出最精简的数据和流程。 + +- 开放世界的场景规范极度依赖于游戏的玩法设计,只有在明确核心角色玩法之后,开发团队才能制定与之对应的 **场景规范** ,确立 **性能优化指标** ,并制作相应的 **工具链** 。以王国之泪为例: + + - 根据角色的精力(游泳,攀爬,滑翔)机制,去定制地形高度,坡度和水体的规范。 + - 根据通天术的技能要求,去定制地形规范。 + - 根据究极手,余料建造,倒转乾坤这些技能的特性,去制作符合其要求的可交互物件。 + - 武器(单手剑,双手剑,长枪,盾牌,箭)可以进行余料建造,在对应的插槽上进行特性绑定,来制定余料物品的规范。 + - 已知角色的状态类别,可以去制作不同的场景效果,比如雷击,着火,寒冷,炎热,并为之提供相应的解决方案(服装 or 料理)。 + - 在没有与核心玩法设计规范出现冲突的前提下, 制作左纳乌科技的玩法。 + + 针对这些规范,引擎开发人员可以进行类似如下的开发工作: + + - 定制`地形可攀爬高度`,`水体可穿越区域`,`通天术可使用区域` 的调试视图(如果UE支持不改源码,能以插件的形式扩展调试视图就很舒服)。 + - 验证各种特效,场景效果的性能指数,调校效果与性能的平衡,并制作相应的管理和验收工具。 + +- 关于编辑器工具,这里有一些小建议: + - 尽可能去追溯问题产生的起源,在源头解决问题的成本是最低的,比如说 **资产创建时(AssetCreated)** , **资产保存时(AssetSaved)** , **Actor创建时(NewActorsPlaced)** ... + - 使用可忽略的 **通知(Notification)** ,而不是使用 **对话框(Dialog)** 强制阻塞用户输入。 + - 提供友好的用户体验,支持撤销,重做,批处理,自定义。 + +## 玩法设计 + +关于开放世界的关卡设计,这里有一些文档: + +- [制作一个开放世界的游戏有哪些挑战?- 天美工作室 / 江岸栖](https://www.zhihu.com/question/336988349/answer/2174068590) +- [从零开始,做一款开放世界 - 游戏葡萄 / wenlon](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMjM5OTc2ODUxMw%3D%3D%26mid%3D2649897301%26idx%3D1%26sn%3D87e94966851b1d3a3e7abad542be6689) +- [游戏开发杂谈-开放世界 - 知乎 / Italink](https://zhuanlan.zhihu.com/p/648572742) +- [元宇宙杂谈 - 知乎 ](https://zhuanlan.zhihu.com/p/663939427) + +开放世界的 **底层玩法设计** 非常重要,它应该是 **完备的,充满取巧** 的,为了达成某一效果而强塞不合理的模块只会给后续的开发工作造成 **毁灭性** 的打击。 + +- 在明确各种预算(时间,精力,金钱,算力)的上限后,制定好 **资源的调度分配** , **取舍是必然的** 。 +- 如果底层玩法设计不严谨,会让整个 **制作流程变得混乱甚至失控** ,随着玩法的堆叠,开放场景的 **制作难度会以指数级提升** ,如果不采用一些 **取巧** 的设计来让玩法内容收敛,将导致 **制作成本很容易飙升** 。 + +举两个例子: + +- 塞尔达中通过底层的系统化玩法架起整个游戏的玩法体系,套用各种模板来完成各种玩法的扩充,开发者保证了 模板和系统化设计 在可管控的范围之内,艺术家和游戏创作者在这个 **限定范围之内** 去做游戏内容的衍生,才使得项目的复杂度不会随着体量变大而急剧上升。 + +- 黑神话中,悟空可以通过 72变 可以变成各种精英怪,这一玩法设计使得研发团队可以将更多的制作成本投入到精英怪的设计当中。 + diff --git "a/Docs/04-UnrealEngine/7.\351\237\263\351\242\221\345\274\200\345\217\221.md" "b/Docs/04-UnrealEngine/7.\351\237\263\351\242\221\345\274\200\345\217\221.md" new file mode 100644 index 00000000..605cc9c0 --- /dev/null +++ "b/Docs/04-UnrealEngine/7.\351\237\263\351\242\221\345\274\200\345\217\221.md" @@ -0,0 +1,1188 @@ +--- +comments: true +--- + +# 音频开发 + +长久以来,渲染一直是图形引擎的热点,而音频似乎很难吸引开发者的注意力,但实际上,音频除了可以增加沉浸式的交互体验,还能进一步增加画面的表现力: + + + +笔者也正是因此,点燃了对程序开发的热情,为了让音频能有更好的图形表现效果,这才入了图形引擎的坑~ + +本篇文章会从基础的音频知识开始讲解,并介绍UE5中常用的音频接口,以及一些常见的 **数字信号处理(Digital Signal Process)** 算法,并且最后 —— 拥抱 **Meta Sound** 。 + +> 笔者并非专业的音频开发人员,如有纰漏,烦请赐教~ + +## 认识音频 + +### 什么是音频? + +众所周知,声音是由震动产生的,就像是这样: + +![How Does Sound Travel From One Medium To Another?](Resources/OIP-C.jpeg) + +> 图片源自:[How Does Sound Travel From One Medium To Another? (scienceabc.com)](https://www.scienceabc.com/pure-sciences/movement-of-sound-waves-through-different-media.html) + +这里有两个视频很好地讲解了声音的本质: + +- [What is Sound-The Dr. Binocs Show](https://www.bilibili.com/video/BV16C4y187Xy) + +- [What is Sound-BranchEducation](https://www.bilibili.com/video/BV1pR4y1L7WD) + +我们通常会将声音视作一个连续的波形,就像是这样: + +![image-20240204102619133](Resources/image-20240204102619133.png) + +计算机的存储精度是有限的(0到1之间有无限个小数),如果我们想要把声音数据存储到计算机中,将会面临两个维度的精度处理问题: + +- 时间 +- 振幅 + +这会使用到 [脉冲编码调制技术(Pulse Code Modulation,简称PCM)](https://baike.baidu.com/item/脉冲编码调制/5143788?fromModule=lemma_inlink) 来将声音的 **连续信号(Continuous Signal)** 转换为 **离散信号(Discrete Signal)** + +首先,我们会通过 **采样(Sampling)** 处理时间维度上的精度分割: + +![image-20240204103616957](Resources/image-20240204103616957.png) + +时间维度的采样精度参数是 **采样率(Sample Rate)** ,它代表单位时间(1s)内对声音信号的采样次数 ,常见的采样率有`44100 Hz`,`48000Hz`... + +得到采样的数据后,会进一步通过 **量化(Quantize)** 处理振幅维度上的精度分割: + +![image-20240204104205505](Resources/image-20240204104205505.png) + +振幅维度的采样精度是 **位深(Bits Per Sample)** ,它代表一个音频样本的bit存储精度,常见的位深有`8 bit`,`16 bit`,`24 bit`,`32 bit`,以 `8 bit`为例,它等价于`1字节`,能表示的数有`256(2^8)`个,算上符号位,它的数值表示范围是`[-128,127]`,可以视为它具有128个振幅梯度。 + +关于采样和量化,这里有一篇优秀的文章: + +- [音视频开发基础入门|声音的采集与量化、音频数字信号质量、音频码率 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/577850804) + +最终我们得到的数据一般称之为 **波形表/图(Wave Table)** ,如果不考虑位深的存储精度,使用浮点表示振幅,在C++中,我们可以表示为这样的存储结构: + +``` c++ +std::vector WaveTable = { + sample0,sample1,sample2,sample3,sample4... +} +``` + +打开电脑的声音设置,我们可以发现每个音频设备(输入或输出),都具有相关的配置: + +![image-20240204105850144](Resources/image-20240204105850144.png) + +在现实世界中, **音源(Sound Source)** 会向四周迸发出大量的运动粒子,一部分粒子会直接进入到人耳,还有一部分粒子会与周边物体发生一次或多次碰撞再进入到人耳中,在这种复杂的周边环境下,通常会导致人的两只耳朵会接受到不同的音频数据,这也为音频营造出了所谓的 **空间感** 。 + +因此,为了在音频存储到计算机时保留这种空间感,我们会存储多份音频数据,这个份数,我们通常称为 **通道数(Number Of Channels)** ,只有一个通道的音频我们称为 **单声道(Mono)** ,具有`2个通道`的音频我们称为 **立体声(Stereo)** 。 + +多个通道的音频样本是交叉组合在一起的,每一个组合,我们称之为 **音频块(Block)** + +对于立体声,它的存储结构如下: + +``` c++ +std::vector WaveTable = { + channel0_sample0, channel1_sample0, // Block 0 + channel0_sample1, channel1_sample1, // Block 1 + channel0_sample2, channel1_sample2, // Block 2 + channel0_sample3, channel1_sample3, // Block 3 + channel0_sample4, channel1_sample4, // Block 4 + ... +} +``` + +四声道同理: + +``` c++ +std::vector WaveTable = { + channel0_sample0, channel1_sample0, channel2_sample0, channel3_sample0, // Block 0 + channel0_sample1, channel1_sample1, channel2_sample1, channel3_sample1, // Block 1 + channel0_sample2, channel1_sample2, channel2_sample2, channel3_sample2, // Block 2 + channel0_sample3, channel1_sample3, channel2_sample3, channel3_sample3, // Block 3 + channel0_sample4, channel1_sample4, channel2_sample4, channel3_sample4, // Block 4 + ... +} +``` + +通常情况下, Wave Table 是不可以直接被音频设备播放的,因为它是一堆波形数据,而不包括音频的格式信息,即采样率,位深,通道数... + +想要它能被播放,我们还需要进一步将其编码为音频文件。 + +### WAVA + +`*.WAV`是最常见的声音文件格式之一,它是Microsoft专为Windows开发的一种标准数字音频文件,它不同于`*.mp3`,`*.flac`,`*.aac` 等音频格式,它是一种无压缩的音频,仅仅是在WaveTable的数据之前,存储了音频的格式: + +![The structure of .wav file format. | Download Scientific Diagram](Resources/The-structure-of-wav-file-format.png) + +这里有一份完整的文档: + +- [WAV文件格式解析及处理 - 简书 (jianshu.com)](https://www.jianshu.com/p/63d7aa88582b) + +请仔细阅读上文,理解WAV格式中各个数据的意义。 + +之后,我们可以在C++中使用如下的内存对齐来描述WAV的文件头结构: + +``` c++ +struct WavFmtChunk { + uint8_t chunkID[4] = { 'f','m','t',' ' }; + int32_t chunkSize; + int16_t audioFormat; + int16_t numChannels; + int32_t sampleRate; + int32_t byteRate; + int16_t blockAlign; + int16_t bitsPerSample; +}; + +struct WavDataChunkHeader { + uint8_t chunkID[4] = { 'd','a','t','a' }; + int32_t chunkSize; +}; + +struct WavRiffChunk { + uint8_t chunkID[4] = { 'R','I','F','F' }; + int32_t chunkSize; + uint8_t format[4] = { 'W','A','V','E' }; + WavFmtChunk fmtChunk; + WavDataChunkHeader dataChunkHeader; +}; + +typedef WavRiffChunk WavHeader; +``` + +在C++中存储一段数据,我们通常会使用`std::vector` ,如果想要将一段波形数据写入到Wav文件中,就可以使用这样的函数: + +``` c++ +void writeWavFile(std::string filePath, int numChannels, int sampleRate, int bitsPerSample, std::vector waveTable) { + WavHeader header; + header.fmtChunk.audioFormat = 0x0001; // PCM格式对应的数值为0x0001 + header.fmtChunk.chunkSize = 16; // PCM格式的音频格式占16字节,如果是其他格式,可能会>16,将导致总的文件头尺寸>44 + header.fmtChunk.numChannels = numChannels; + header.fmtChunk.bitsPerSample = bitsPerSample; + header.fmtChunk.sampleRate = sampleRate; + header.fmtChunk.blockAlign = header.fmtChunk.numChannels * header.fmtChunk.bitsPerSample / 8; + header.fmtChunk.byteRate = header.fmtChunk.sampleRate * header.fmtChunk.blockAlign; + header.dataChunkHeader.chunkSize = waveTable.size(); + header.chunkSize = sizeof(header.format) + sizeof(header.fmtChunk) + sizeof(header.dataChunkHeader) + waveTable.size(); + std::ofstream out(filePath, std::ios::out | std::ios::binary); //务必使用std::ios::binary,否则数据错乱 + assert(out.is_open() && "Can`t Write File"); + out.write(reinterpret_cast(&header), sizeof(WavHeader)); + out.write(reinterpret_cast(waveTable.data()), waveTable.size()); + out.close(); +} + +``` + +读取也非常简单: + +``` c++ +std::vector readWavFile(std::string filePath) { + WavHeader header; + std::ifstream in(filePath, std::ios::in | std::ios::binary); //务必使用std::ios::binary,否则数据错乱 + assert(in.is_open() && "Can`t Read File"); + in.read(reinterpret_cast(&header), sizeof(header)); + int waveTableSize = header.dataChunkHeader.chunkSize; //获取到波形数据的大小 + std::vector waveTable(waveTableSize); + in.read(reinterpret_cast(waveTable.data()), waveTableSize); + return waveTable; +} +``` + +接下来我们来生成一段音频波形, **正弦波(Sine Wave)** 是基本的波形之一,它的函数式如下: +$$ +y(Time) = Amplitude * sin(2 * PI * Frequency * Time + PhaseOffset) + Bias +$$ +根据函数式,我们可以编写如下的函数: + +```C++ +const double M_PI = 3.1415926535; +std::vector generateSineWave(int timeLengthSec, // 正弦波的时长,单位为秒 + int frequency, // 正弦波的频率 + float amplitude, // 正弦波的振幅 + int numChannels, // 音频的通道数 + int sampleRate, // 音频的采样率 + int bitsPerSample // 音频的位深 + ) { + int numBlocks = timeLengthSec * sampleRate; // 音频块的数量 + amplitude = std::clamp(amplitude, 0.01f, 1.0f); // 限制振幅的大小, std::clamp 需要C++17 + std::vector waveTable; + waveTable.resize(numBlocks * numChannels * bitsPerSample / 8); // 算出音频数据所需的内存大小 + int step = bitsPerSample / 8; + for (int i = 0; i < numBlocks; i++) { + float time = i / (float)numBlocks * timeLengthSec; // 时间点 + float sampleValue = amplitude * std::sin(2 * M_PI * frequency * time); //算出对应时间点的采样值 + // 我们得想个办法将sampleValue写入到waveTable里面 + } + return waveTable; +} +``` + +上面的代码里面我们面临一个难题: + +- 通过正弦波的函数,我们会得到一个类型为`float`的采样值,但位深可能是`8 bit`,`16 bit`,`24 bit`,`32 bit`,我们该怎么把float转换为对应位深的数据呢? + +常见的做法借助已有的数据类型来进行对应数据位的赋值,就像是这样: + +``` c++ +const int PCMS16MaxAmplitude = 32767; // 16 bit的数值范围是[-32768,32767],这里取最小绝对值(32767)避免越界 + +void write16BitSampleValue(uint8_t* dataPtr, float sampleValue) { + (*reinterpret_cast(dataPtr)) = sampleValue * PCMS16MaxAmplitude; +} +``` + +也可以利用位运算来进行填充: + +``` c++ +const int PCMS24MaxAmplitude = 8388607; +void write24BitSampleValue(uint8_t* dataPtr, float sampleValue) { + int value = sampleValue * PCMS24MaxAmplitude; + dataPtr[0] = value & 0xFF; + dataPtr[1] = (value >> 8) & 0xFF; + dataPtr[2] = (value >> 16) & 0xFF; +} +``` + +综上,我们可以整理出一个这样的转换函数表: + +``` c++ +const int PCMS8MaxAmplitude = 127; +const int PCMS16MaxAmplitude = 32767; +const int PCMS24MaxAmplitude = 8388607; +const int PCMS32MaxAmplitude = 2147483647; + +typedef std::function SampleValueWriteFunction; +static std::map SampleValueWriteFunctions = { + {8,[](uint8_t* dataPtr, float sampleValue) { + // 笔者不清楚为什么这里需要加上一个偏差波形才是正确的=.= + (*reinterpret_cast(dataPtr)) = sampleValue * PCMS8MaxAmplitude + PCMS8MaxAmplitude; + }}, + {16,[](uint8_t* dataPtr, float sampleValue) { + (*reinterpret_cast(dataPtr)) = sampleValue * PCMS16MaxAmplitude; + }}, + {24,[](uint8_t* dataPtr, float sampleValue) { + int value = sampleValue * PCMS24MaxAmplitude; + dataPtr[0] = value & 0xFF; + dataPtr[1] = (value >> 8) & 0xFF; + dataPtr[2] = (value >> 16) & 0xFF; + }}, + {32,[](uint8_t* dataPtr, float sampleValue) { + (*reinterpret_cast(dataPtr)) = sampleValue * PCMS32MaxAmplitude; + }} +}; + +typedef std::function SampleValueReadFunction; +static std::map SampleValueReadFunctions = { + {8,[](uint8_t* dataPtr) { + return ((*reinterpret_cast(dataPtr)) - PCMS8MaxAmplitude) / float(PCMS8MaxAmplitude); + }}, + {16,[](uint8_t* dataPtr) { + return (*reinterpret_cast(dataPtr)) / float(PCMS16MaxAmplitude); + }}, + {24,[](uint8_t* dataPtr) { + int value = dataPtr[2]; + value <<= 8; + value |= dataPtr[1]; + value <<= 8; + value |= dataPtr[0]; + return value / float(PCMS24MaxAmplitude); + }}, + {32,[](uint8_t* dataPtr) { + return (*reinterpret_cast(dataPtr)) / float(PCMS32MaxAmplitude); + }} +}; +``` + +综上,加上测试代码,完整的代码如下: + +``` c++ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct WavFmtChunk { + uint8_t chunkID[4] = { 'f','m','t',' ' }; + int32_t chunkSize; + int16_t audioFormat; + int16_t numChannels; + int32_t sampleRate; + int32_t byteRate; + int16_t blockAlign; + int16_t bitsPerSample; +}; + +struct WavDataChunkHeader { + uint8_t chunkID[4] = { 'd','a','t','a' }; + int32_t chunkSize; +}; + +struct WavRiffChunk { + uint8_t chunkID[4] = { 'R','I','F','F' }; + int32_t chunkSize; + uint8_t format[4] = { 'W','A','V','E' }; + WavFmtChunk fmtChunk; + WavDataChunkHeader dataChunkHeader; +}; + +typedef WavRiffChunk WavHeader; + +const int PCMS8MaxAmplitude = 127; +const int PCMS16MaxAmplitude = 32767; +const int PCMS24MaxAmplitude = 8388607; +const int PCMS32MaxAmplitude = 2147483647; + +typedef std::function SampleValueWriteFunction; +static std::map SampleValueWriteFunctions = { + {8,[](uint8_t* dataPtr, float sampleValue) { + (*reinterpret_cast(dataPtr)) = sampleValue * PCMS8MaxAmplitude + PCMS8MaxAmplitude; // 笔者不清楚为什么这里需要加上一个偏差波形才是正确的=.= + }}, + {16,[](uint8_t* dataPtr, float sampleValue) { + (*reinterpret_cast(dataPtr)) = sampleValue * PCMS16MaxAmplitude; + }}, + {24,[](uint8_t* dataPtr, float sampleValue) { + int value = sampleValue * PCMS24MaxAmplitude; + dataPtr[0] = value & 0xFF; + dataPtr[1] = (value >> 8) & 0xFF; + dataPtr[2] = (value >> 16) & 0xFF; + }}, + {32,[](uint8_t* dataPtr, float sampleValue) { + (*reinterpret_cast(dataPtr)) = sampleValue * PCMS32MaxAmplitude; + }} +}; + +typedef std::function SampleValueReadFunction; +static std::map SampleValueReadFunctions = { + {8,[](uint8_t* dataPtr) { + return ((*reinterpret_cast(dataPtr)) - PCMS8MaxAmplitude) / float(PCMS8MaxAmplitude); + }}, + {16,[](uint8_t* dataPtr) { + return (*reinterpret_cast(dataPtr)) / float(PCMS16MaxAmplitude); + }}, + {24,[](uint8_t* dataPtr) { + int value = dataPtr[2]; + value <<= 8; + value |= dataPtr[1]; + value <<= 8; + value |= dataPtr[0]; + return value / float(PCMS24MaxAmplitude); + }}, + {32,[](uint8_t* dataPtr) { + return (*reinterpret_cast(dataPtr)) / float(PCMS32MaxAmplitude); + }} +}; + +const double M_PI = 3.1415926535; + +std::vector generateSineWave(int timeLengthSec, int frequency, float amplitude, int numChannels, int sampleRate, int bitsPerSample) { + int numBlocks = timeLengthSec * sampleRate; // 音频块的数量 + amplitude = std::clamp(amplitude, 0.01f, 1.0f); // 限制振幅的大小, std::clamp 需要C++17 + std::vector waveTable; + waveTable.resize(numBlocks * numChannels * bitsPerSample / 8); + int step = bitsPerSample / 8; //计算每个样本的字节跨度 + uint8_t* waveTablePtr = waveTable.data(); + SampleValueWriteFunction writer = SampleValueWriteFunctions.at(bitsPerSample); + for (int i = 0; i < numBlocks; i++) { + float time = i / (float)numBlocks * timeLengthSec; // 当前音频块所对应的时间 + float sampleValue = amplitude * std::sin(2 * M_PI * frequency * time); //当前时间对应的正弦值 + for (int channelIndex = 0; channelIndex < numChannels; channelIndex++) { //所有通道都使用一样的值 + writer(waveTablePtr, sampleValue); + waveTablePtr += step; + } + } + return waveTable; +} + +void writeWavFile(std::string filePath, int numChannels, int sampleRate, int bitsPerSample, std::vector waveTable) { + WavHeader header; + header.fmtChunk.audioFormat = 0x0001; // PCM格式对应的数值为0x0001 + header.fmtChunk.chunkSize = 16; // PCM格式的音频格式占16字节,如果是其他格式,可能会>16,将导致总的文件头尺寸>44 + header.fmtChunk.numChannels = numChannels; + header.fmtChunk.bitsPerSample = bitsPerSample; + header.fmtChunk.sampleRate = sampleRate; + header.fmtChunk.blockAlign = header.fmtChunk.numChannels * header.fmtChunk.bitsPerSample / 8; + header.fmtChunk.byteRate = header.fmtChunk.sampleRate * header.fmtChunk.blockAlign; + header.dataChunkHeader.chunkSize = waveTable.size(); + header.chunkSize = sizeof(header.format) + sizeof(header.fmtChunk) + sizeof(header.dataChunkHeader) + waveTable.size(); + std::ofstream out(filePath, std::ios::out | std::ios::binary); //务必使用std::ios::binary,否则数据错乱 + assert(out.is_open() && "Can`t Write File"); + out.write(reinterpret_cast(&header), sizeof(WavHeader)); + out.write(reinterpret_cast(waveTable.data()), waveTable.size()); + out.close(); +} + +std::vector readWavFile(std::string filePath) { + WavHeader header; + std::ifstream in(filePath, std::ios::in | std::ios::binary); //务必使用std::ios::binary,否则数据错乱 + assert(in.is_open() && "Can`t Read File"); + in.read(reinterpret_cast(&header), sizeof(header)); + int waveTableSize = header.dataChunkHeader.chunkSize; //获取到波形数据的大小 + std::vector waveTable(waveTableSize); + in.read(reinterpret_cast(waveTable.data()), waveTableSize); + return waveTable; +} + +int main() { + assert(sizeof(WavHeader) == 44 && "WAV Header Size Error"); + + int numChannels = 2; //单通道 + int sampleRate = 48000; //采样率为48000HZ + int bitsPerSample = 16; //每个音频样本占16Bit,即2字节 + int auidoLengthSec = 2; //生成2s的正弦波 + int sineFrequency = 440; //正弦波的频率 + float sineAmplitude = 0.8f; //正弦波的振幅 + + std::vector waveTable = generateSineWave(auidoLengthSec, sineFrequency, sineAmplitude, numChannels, sampleRate, bitsPerSample); + + writeWavFile("./sineWave.wav", numChannels, sampleRate, bitsPerSample, waveTable); + + std::vector readData = readWavFile("./sineWave.wav"); + + assert(readData == waveTable && "Audio Data Error"); + + return 0; +} +``` + +执行该程序会在输出目录生成一个`sineWave.wav`音频文件,尝试播放它~ + +![image-20240205181636978](Resources/image-20240205181636978.png) + +你还可以修改一下参数,重新生成音频,听听不同频率下的正弦波有什么区别。 + +此外,你还可以尝试模拟空间音频的效果: + +``` c++ +std::vector generateSineSurroundWave(int timeLengthSec, int frequency, float amplitude, int numChannels, int sampleRate, int bitsPerSample) { + assert(numChannels == 2); + + int numBlocks = timeLengthSec * sampleRate; // 音频块的数量 + amplitude = std::clamp(amplitude, 0.01f, 1.0f); // 限制振幅的大小, std::clamp 需要C++17 + std::vector waveTable; + waveTable.resize(numBlocks * numChannels * bitsPerSample / 8); + int step = bitsPerSample / 8; //计算每个样本的字节跨度 + uint8_t* waveTablePtr = waveTable.data(); + SampleValueWriteFunction writer = SampleValueWriteFunctions.at(bitsPerSample); + + float leftEarPos[2] = { -5,0 }; // 左耳的位置(X,Y) + float rightEarPos[2] = { 5,0 }; // 右耳的位置(X,Y) + float radiusOfSurround = 10; // 环绕音频的半径 + float audioAttenuationFactor = 0.7; // 声音的衰减因子,例如0.7,代表每隔一个单位长度,音频会衰减为原来的0.7倍 + float periodSec = 2; // 旋转一圈的耗时,单位为秒,2代表每两秒旋转一圈 + + for (int i = 0; i < numBlocks; i++) { + float time = i / (float)numBlocks * timeLengthSec; // 当前音频块所对应的时间 + float sampleValue = amplitude * std::sin(2 * M_PI * frequency * time); // 当前时间对应的正弦值 + + float angle = 2 * M_PI * time / periodSec; // 算出当前旋转的角度 + float audioSourcePos[2] = { // 算出当前音源所在的位置 + radiusOfSurround * std::cos(angle), + radiusOfSurround * std::sin(angle) + }; + + float leftEarDistance = sqrt(pow(leftEarPos[0] - audioSourcePos[0], 2) + pow(leftEarPos[1] - audioSourcePos[1], 2)); // 音源离左耳的距离 + float leftSampleValue = sampleValue * pow(audioAttenuationFactor, leftEarDistance); // 根据距离,算出衰减后的音频样本值 + writer(waveTablePtr, leftSampleValue); // 写入左声道 + waveTablePtr += step; + + float rightEarDistance = sqrt(pow(rightEarPos[0] - audioSourcePos[0], 2) + pow(rightEarPos[1] - audioSourcePos[1], 2)); // 音源离右耳的距离 + float rightSampleValue = sampleValue * pow(audioAttenuationFactor, rightEarDistance); // 根据距离,算出衰减后的音频样本值 + writer(waveTablePtr, rightSampleValue); // 写入右声道 + waveTablePtr += step; + } + return waveTable; +} + +int main() { + assert(sizeof(WavHeader) == 44 && "WAV Header Size Error"); + int numChannels = 2; //单通道 + int sampleRate = 48000; //采样率为48000HZ + int bitsPerSample = 16; //每个音频样本占16Bit,即2字节 + int auidoLengthSec = 2; //生成2s的正弦波 + int sineFrequency = 150; //正弦波的频率 + float sineAmplitude = 0.8f; //正弦波的振幅 + std::vector waveTable = generateSineSurroundWave(auidoLengthSec, sineFrequency, sineAmplitude, numChannels, sampleRate, bitsPerSample); + writeWavFile("./sineWave.wav", numChannels, sampleRate, bitsPerSample, waveTable); + std::vector readData = readWavFile("./sineWave.wav"); + assert(readData == waveTable && "Audio Data Error"); + return 0; +} +``` + +在这个网站上,可以上传文件查看音频的波形: + +- [Musiscope (sciencemusic.org)](https://oscilloscope.sciencemusic.org/) + +![image-20240204142115830](Resources/image-20240204142115830.png) + +至此,我们应该大致搞明白了音频在计算机上的原始形态。 + +## 音频框架 + +### 音频IO接口 + +有了音频,我们还需要清楚,怎么跟音频设备打交道,而常见的音频设备有: + +- 输入:麦克风 +- 输出:扬声器,音响,耳机 + +这些设备厂商会提供或支持相关的音频驱动,通常情况下,我们无需直接跟音频驱动打交道,而是利用操作系统提供的音频接口来处理音频的输入输出,例如: + +- **Windows:** DirectSound,ASIO,WASAPI +- **Linux: ** native ALSA,Jack, OSS +- **Mac OS:** CoreAudio ,Jack + +为了跨平台的音频处理,很多框架也会对这些接口进行统一的封装,就比如: + +- **Qt Multimedia** 中的音频IO模块:[Audio Overview | Qt Multimedia 6.6.1](https://doc.qt.io/qt-6/audiooverview.html) +- **UE** 引入的 **RtAudio** : http://www.music.mcgill.ca/~gary/rtaudio/ + +### 傅里叶变换库 + +傅里叶变换(Fourier Transform)是信号处理的重要工具。 + +已知声音是一段随时间变换的连续信号,就像是这样: + +![image-20240205162859891](Resources/image-20240205162859891.png) + +但我们听到的声音往往不会只有一种频率,实际上,可能会是由很多频率的声波堆叠到一起,就像是这样: + +![image-20240205162920488](Resources/image-20240205162920488.png) + +而傅里叶能帮助我们,分析 **一段时间内** 已经混合了大量不同频率波形 的 **时域信号(Time Domain Signal)** ,得出该时间段内声音波形的频率分布,即 **频域信号(Frequency Domain Signal)** ,整个过程看起来就像是这样的: + +![TikZ 绘制傅里叶级数示意图 - LaTeX 工作室](Resources/9796875423a0a9928413d91f8c7420c2.png) + +这里有一个非常好的视频讲解了傅里叶变换的原理: + +- [【官方双语】形象展示傅里叶变换 |3Blue1Brown](https://www.bilibili.com/video/BV1pW411J7s8) + +一些看起来比较混乱的时序信号,在转换到频域之后,我们会更容易发现数据的特征,就比如这是一段音乐的实时分析结果: + +> 虽然听不到声音,但相信你也能从跃动的图形能看出音乐的节奏变化。 + +![sfsa](Resources/sfsa.gif) + +此外,傅里叶变换不仅仅是一个数学处理的工具,它还引导我们在频域去思考问题,如果你喜欢玩 Shader Toy 或 GLSL Sandbox,里面很多代码其实都是基于频域思考的。 + +这里有一个有趣的视频,每条线都在按固定的频率匀速转动: + +- [傅里叶变换之美 | 科学小视](https://www.bilibili.com/video/BV1So4y1T7ae) + +这里有一些比较成熟的傅里叶算法实现: + +- [fftw3](https://github.com/FFTW/fftw3):西方最快的傅里叶变换库 +- [pffft](https://bitbucket.org/jpommier/pffft):一个小巧的傅里叶变换库 +- [ffts](https://github.com/anthonix/ffts):南方最快的傅里叶变换库(狗头) + +如果你也像笔者一样喜欢音频可视化,这里推荐两个开源库: + +- [adamstark/Gist: A C++ Library for Audio Analysis (github.com)](https://github.com/adamstark/Gist) +- [jmerkt/rt-cqt: Real-Time Constant-Q Transform (github.com)](https://github.com/jmerkt/rt-cqt) + +### DSP算法库 + +DSP的全称是 **数字信号处理(Digital Signal Processor)** + +在 `认识音频` 的小节中,我们生成了一个正弦波,并写入到wav文件,通常情况下,这些功能会囊括在DSP库中,此外,它往往还会提供一些其他功能,例如: + +- 滤波器 +- 重采样 +- 音频合成 +- 频谱分析 +- ... + +开源DSP库有: + +- [KFR | Fast, modern C++ DSP framework (kfrlib.com)](https://www.kfrlib.com/) +- [Maximilian | C++ Audio and Music DSP Library](https://github.com/micknoise/Maximilian) + +### 音频开发引擎 + +通常情况下,对于游戏项目的音频开发人员,无需了解上面的一些底层接口和框架,一般游戏引擎都会提供基本的音频模块,涉及到复杂的音频机制,我们会直接使用音频引擎提供的编辑器来进行开发。 + +常见的音频引擎有: + +- [CRIWARE ADX](https://www.criware.com/en/products/adx2.html):ADX 几乎没有学习曲线,因为它的创作工具充分利用了 DAW 范式所提供的功能,并添加了直观的功能来创建交互式声音。在项目规模越来越大、期限越来越紧的时候,使用 ADX 提高您的工作效率! +- [FMOD](https://fmod.com/):FMOD 是一种端到端解决方案,用于为任何游戏添加声音和音乐。使用 FMOD Studio 构建自适应音频,并使用 FMOD 引擎在游戏中播放。 +- [Wwise | Audiokinetic](https://www.audiokinetic.com/zh/products/wwise/):可提供一整套互动音频设计与开发工具,无论项目规模如何都能帮您轻松完成原型设计,并在此基础上将自己的音频创作构想变为现实。 + +这些音频软件都会提供独特的编辑器,并在相关的游戏引擎(UE,Unity,Cocos...)中提供插件支持。 + +关于音频引擎的选择,知乎上面有一些相关的讨论: + +- [有哪些国内的游戏使用了音频引擎来做音频部分的开发?比如FMOD和wwise? - 知乎 (zhihu.com)](https://www.zhihu.com/question/29992299) + +- [使用Unity5开发手游,使用FMOD或wwise音频插件而不是自带的声音系统有哪些好处? - 知乎 (zhihu.com)](https://www.zhihu.com/question/61882604) + +**Meta Sound** 算是UE5提供的一个内置的音频引擎,虽然目前它的功能并不能媲美上面的几个音频引擎,但这种一站式的解决方案,无疑会让UE变得越来越强大。 + +就像 UE5 的建模工具一样,其最大的价值不是让美术人员也能在UE里进行建模,本质上最大的受益者其实是开发人员(应该算TA?) + +开发人员可以针对自身项目的场景资产在编辑器下进行快速的算法效果验证,在策略稳定后,利用UE底层提供的一系列网格处理算法,搭建出一条从DCC模型到高品质引擎资产的自动化管线。 + +> 在之前开放世界的章节提到,HLOD是将Cell中的所有模型合成一个低精度的模型,在具有复杂类别的场景中使用统一的策略往往很难得到好的效果,在熟悉了UE底层的网格处理算法,如果有恰当的资产分类,想要生成高品质的HLOD并不困难。 + +## UE 音频 + + 在 Unreal Fest 2023 ,大佬介绍了 UE 的音频架构: + +- [Sounds Good: Unreal Engine Audio | Unreal Fest 2023](https://www.bilibili.com/video/BV1EC4y1J7Bs) + +![image-20240204155736479](Resources/image-20240204155736479.png) + +我们能从如下站点获取到一些信息,但可惜的是,虚幻中关于音频的文档相对而言比较少,并且混杂着大量已过时的文档: + +- [在虚幻引擎中使用音频 |虚幻引擎 5.3 文档 (unrealengine.com)](https://docs.unrealengine.com/5.3/en-US/working-with-audio-in-unreal-engine/) +- [Latest unreal-engine topics in Audio - Epic Developer Community Forums (unrealengine.com)](https://forums.unrealengine.com/tags/c/development-discussion/audio/42/unreal-engine) + +### 音频设备 + +音频设备分为输入设备和输出设备,它们的相关信息分别存储在UE的 **Audio::FCaptureDeviceInfo** 和 **Audio::FAudioPlatformDeviceInfo** ,我们可以通过如下接口获取到所有的设备: + +``` c++ +// 获取所有的输入设备 +Audio::FAudioCapture AudioCapture; +TArray CaptureDevices; +AudioCapture.GetCaptureDevicesAvailable(CaptureDevices); +for (auto InputDeviceInfo : CaptureDevices) { + UE_LOG(LogTemp, Warning,TEXT("[Input Device] Name: %s, Device Id: %s, Num Channels: %u, Sample Rate: %u, Supports Hardware AEC: %u, "), + *InputDeviceInfo.DeviceName, + *InputDeviceInfo.DeviceId, + InputDeviceInfo.InputChannels, + InputDeviceInfo.PreferredSampleRate, + InputDeviceInfo.bSupportsHardwareAEC); +} + +// 获取当前(默认)音频输入设备 +Audio::FCaptureDeviceInfo CurrentInputDevice; +AudioCapture.GetCaptureDeviceInfo(CurrentInputDevice); +UE_LOG(LogTemp, Warning, TEXT("[Current Input Device] %s"), + *CurrentInputDevice.DeviceName); + +// 获取所有的输出设备 +Audio::FMixerDevice* AudioMixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(GWorld); //使用当前GWorld作为WorldContentObject +if (AudioMixerDevice){ + if (Audio::IAudioMixerPlatformInterface* MixerPlatform = AudioMixerDevice->GetAudioMixerPlatform()){ + uint32 NumOutputDevices = 0; + MixerPlatform->GetNumOutputDevices(NumOutputDevices); + for (uint32 i = 0; i < NumOutputDevices; ++i){ + Audio::FAudioPlatformDeviceInfo OutputDeviceInfo; + MixerPlatform->GetOutputDeviceInfo(i, OutputDeviceInfo); + UE_LOG(LogTemp, Warning, TEXT("[Output Device] Name: %s, Device Id: %s, Num Channels: %u, Sample Rate: %u, "), + *OutputDeviceInfo.Name, + *OutputDeviceInfo.DeviceId, + OutputDeviceInfo.NumChannels, + OutputDeviceInfo.SampleRate); + } + + // 获取当前(默认)音频输出设备 + Audio::FAudioPlatformDeviceInfo CurrentOutputDevice = MixerPlatform->GetPlatformDeviceInfo(); + UE_LOG(LogTemp, Warning, TEXT("[Current Output Device] %s"), + *CurrentOutputDevice.Name); + } +} +``` + +![image-20240204170415081](Resources/image-20240204170415081.png) + +### 输出音频 + +UE 中的音源基类是 **USoundBase** : + +![image-20240204191436270](Resources/image-20240204191436270.png) + +引擎中有许多派生: + +![image-20240204191454472](Resources/image-20240204191454472.png) + +我们的关注点主要在 **USoundWave** 上,它会将波形数据传输给音频设备进行播放。 + +结合我们之前编写的正弦波生成函数,我们也可以在UE里播放我们自定义的波形数据,就像是这样: + +``` c++ +void UTestBlueprintLibrary::PlayTestAudio(UObject* WorldContext) // 用于测试的蓝图函数库 +{ + int numChannels = 2; //单通道 + int sampleRate = 48000; //采样率为48000HZ + int bitsPerSample = 16; //每个音频样本占16Bit,即2字节 + int auidoLengthSec = 2; //生成2s的正弦波 + int sineFrequency = 440; //正弦波的频率 + float sineAmplitude = 0.8f; //正弦波的振幅 + + USoundWaveProcedural* Wave = NewObject(); + Wave->NumChannels = numChannels; + Wave->SampleByteSize = bitsPerSample / 8; // UE只支持int16 + Wave->SetSampleRate(sampleRate); + std::vector waveTable = generateSineWave(auidoLengthSec, + sineFrequency, + sineAmplitude, + numChannels, + sampleRate, + bitsPerSample); + Wave->QueueAudio(waveTable.data(), waveTable.size()); + UGameplayStatics::PlaySound2D(WorldContext, Wave); +} +``` + +这里用到了函数`UGameplayStatics::PlaySound2D`,我们可以看下它的内部实现: + +``` c++ +void UGameplayStatics::PlaySound2D(const UObject* WorldContextObject, + USoundBase* Sound, + float VolumeMultiplier, + float PitchMultiplier, + float StartTime, + USoundConcurrency* ConcurrencySettings, + const AActor* OwningActor, + bool bIsUISound) +{ + if (!Sound || !GEngine || !GEngine->UseSound()) + { + return; + } + + UWorld* ThisWorld = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + if (!ThisWorld || !ThisWorld->bAllowAudioPlayback || ThisWorld->IsNetMode(NM_DedicatedServer)) + { + return; + } + + if (FAudioDeviceHandle AudioDevice = ThisWorld->GetAudioDevice()) // 获取当前世界的音频输出设备 + { + FActiveSound NewActiveSound; // 创建一个激活的音频 + NewActiveSound.SetSound(Sound); + NewActiveSound.SetWorld(ThisWorld); + + NewActiveSound.SetPitch(PitchMultiplier); + NewActiveSound.SetVolume(VolumeMultiplier); + + NewActiveSound.RequestedStartTime = FMath::Max(0.f, StartTime); + + NewActiveSound.bIsUISound = bIsUISound; + NewActiveSound.bAllowSpatialization = false; + + if (ConcurrencySettings) + { + NewActiveSound.ConcurrencySet.Add(ConcurrencySettings); + } + + NewActiveSound.Priority = Sound->Priority; + NewActiveSound.SubtitlePriority = Sound->GetSubtitlePriority(); + + // If OwningActor isn't supplied to this function, derive an owner from the WorldContextObject + const AActor* ActiveSoundOwner = OwningActor ? OwningActor : GameplayStatics::GetActorOwnerFromWorldContextObject(WorldContextObject); + + NewActiveSound.SetOwner(ActiveSoundOwner); + + TArray Params; + UActorSoundParameterInterface::Fill(ActiveSoundOwner, Params); + + AudioDevice->AddNewActiveSound(NewActiveSound, &Params); // 将激活音频扔给输出设备处理 + } +} +``` + +我们调用`USoundWaveProcedural::QueueAudio`添加的Wave数据,会存储到内部的AudioBuffer中,后续会被音频设备调用`USoundWaveProcedural::GeneratePCMData`来接受,并播放声音。 + +``` c++ +int32 USoundWaveProcedural::GeneratePCMData(uint8* PCMData, const int32 SamplesNeeded) +{ + // ... + FMemory::Memcpy((void*)PCMData, &AudioBuffer[0], BytesToCopy); +} +``` + +使用 **USoundWaveProcedural** 我们可以播放PCM数据,但如果涉及到一些复杂的音频生成策略,我们可能需要派生 **USoundWaveProcedural** , **UMetaSoundSource** 的实现是一个很好的参考示例,我们只需自定义一个 **ISoundGenerator** ,覆写它的音频生成函数: + +``` c++ +class ISoundGenerator +{ +public: + ENGINE_API ISoundGenerator(); + ENGINE_API virtual ~ISoundGenerator(); + + // Called when a new buffer is required. + virtual int32 OnGenerateAudio(float* OutAudio, int32 NumSamples) = 0; + + // Returns the number of samples to render per callback + virtual int32 GetDesiredNumSamplesToRenderPerCallback() const { return 1024; } + + // Optional. Called on audio generator thread right when the generator begins generating. + virtual void OnBeginGenerate() {} + + // Optional. Called on audio generator thread right when the generator ends generating. + virtual void OnEndGenerate() {} + + // Optional. Can be overridden to end the sound when generating is finished. + virtual bool IsFinished() const { return false; } + + // Retrieves the next buffer of audio from the generator, called from the audio mixer + ENGINE_API int32 GetNextBuffer(float* OutAudio, int32 NumSamples, bool bRequireNumberSamples = false); + + virtual Audio::AudioTaskQueueId GetSynchronizedRenderQueueId() const { return 0; } + +protected: + + // Protected method to execute lambda in audio render thread + // Used for conveying parameter changes or events to the generator thread. + ENGINE_API void SynthCommand(TUniqueFunction Command); + +private: + + ENGINE_API void PumpPendingMessages(); + + // The command queue used to convey commands from game thread to generator thread + TQueue> CommandQueue; + + friend class USynthComponent; +}; +``` + +再派生自己的 **USoundWaveProcedural** ,覆写: + +``` c++ +virtual ISoundGeneratorPtr USoundBase::CreateSoundGenerator(const FSoundGeneratorInitParams& InParams) { return nullptr; } +``` + +UE的音频设备在播放音频源时,如果它是程序化音频`(USoundWave.bProcedural == true)`,那么将会调用`CreateSoundGenerator`为其创建 **SoundGenerator** : + +![image-20240204195219544](Resources/image-20240204195219544.png) + +在实际解析音频时,会调用如下接口生成音频: + +![image-20240204195409513](Resources/image-20240204195409513.png) + +详细的实现可参考 **UMetaSoundSource** 和 **FMetasoundGenerator** + +### 捕获音频 + +游戏项目中很少会有音频捕获的需求,因此这里只做简单介绍。 + +关于音频捕获,主要分为两类: + +- 捕获系统的音频 +- 捕获游戏内的音频 + +#### 系统音频捕获 + +系统音频捕获 特指 输入设备(麦克风)的采集,UE目前并不支持输出设备的捕获,如果有相应的需求,则需要使用更低级别的API来实现(如WASAPI)。 + +``` c++ +TSharedPtr AudioCapture = MakeShared(); +Audio::FOnCaptureFunction OnCapture = [](const float* AudioData, + int32 NumFrames, + int32 InNumChannels, + int32 InSampleRate, + double StreamTime, + bool bOverFlow) { + // 处理采集到的音频数据,此时是在异步线程 +}; +Audio::FAudioCaptureDeviceParams Params; // 音频采集的参数配置,不填设备则会使用默认输入设备 +if (mAudioCapture->OpenCaptureStream(Params, MoveTemp(OnCapture), 1024)) { // 开始音频捕获 + Audio::FCaptureDeviceInfo Info; + if (mAudioCapture->GetCaptureDeviceInfo(Info)){ + OnAudioFormatChanged.ExecuteIfBound(Info.InputChannels, Info.PreferredSampleRate); // 音频格式变换的通知信号 + } +} +``` + +#### 游戏内音频捕获 + +游戏内音频捕获使用UE提供的接口 **ISubmixBufferListener** : + +``` c++ +/** Abstract interface for receiving audio data from a given submix. */ +class ISubmixBufferListener +{ +public: + virtual ~ISubmixBufferListener() = default; + + /** + Called when a new buffer has been rendered for a given submix + @param OwningSubmix The submix object which has rendered a new buffer + @param AudioData Ptr to the audio buffer + @param NumSamples The number of audio samples in the audio buffer + @param NumChannels The number of channels of audio in the buffer (e.g. 2 for stereo, 6 for 5.1, etc) + @param SampleRate The sample rate of the audio buffer + @param AudioClock Double audio clock value, from start of audio rendering. + */ + virtual void OnNewSubmixBuffer(const USoundSubmix* OwningSubmix, float* AudioData, int32 NumSamples, int32 NumChannels, const int32 SampleRate, double AudioClock) = 0; + + /** + * Called if the submix is evaluating disabling itself for the next buffer of audio in FMixerSubmix::IsRenderingAudio() + * if this returns true, FMixerSubmix::IsRenderingAudio() will return true. + * Otherwise the submix will evaluate playing sounds, children submixes, etc to see if it should auto-disable + * + * This is called every evaluation. + * ISubmixListeners that intermittently render audio should only return true when they have work to do. + */ + virtual bool IsRenderingAudio() const + { + return false; + } +}; +``` + +我们只需派生 **ISubmixBufferListener** ,覆写`OnNewSubmixBuffer`处理音频数据,并将其注册到需要捕获 **FAudioDevice** 的 **USoundSubmix** 即可,这块代码依赖比较多,贴在文章中会比较乱,具体的实现大家可参考 **FNiagaraSubmixListener** 。 + +### DSP + +UE内置了一个非常完善的DSP模块—— **SignalProcessing** + +你几乎能在其中找到你想要的任何东西: + +![image-20240204204639645](Resources/image-20240204204639645.png) + +这是一些常用的结构和函数: + +- **Audio::TCircularAudioBuffer** :环形缓冲区,常用于流式音频的存储。 +- **Audio::TSampleBuffer** :用于将RawPCM数据转换为对应位深的可采样数据,类似于提供之前我们提供的`SampleValueWriteFunction`,不过只支持`int16`和`float`。 +- **Audio::FResampler** :重采样器,用于修改音频的采样率。 +- **Audio::FWindow** :用于生成窗函数,支持`Hamming`,`Hann`,`Blackman`。 +- **Audio::TSlidingWindow** :滑动窗口。 +- **Audio::IFFTAlgorithm** :提供基础的FFT算法,提供`FFFTFactory::NewFFTAlgorithm(...)`创建。 +- **Audio::FBiquadFilter** :双二阶滤波器。 +- **Audio::FSpectrumAnalyzer** :频谱分析仪,支持振幅谱,CQT谱。 +- **Audio::FAsyncSpectrumAnalyzer** :执行在异步线程的频谱分析仪,功能同上。 +- `ConstantQ.h`:提供了一系列支持常量Q变换的结构,CQT常用于音乐的频谱可视化,它相较于FFT有更好的节奏表现效果。 + +对于频谱分析,UE还有一个模块`AudioSynesthesia`来扩展相关的功能。 + +![image-20240205104528872](Resources/image-20240205104528872.png) + +它支持 **CQT** , **Onset(迸发)** , **Loudness(响度)** , **Meter** 的分析,可惜的是大多是一些 **NRT(非实时)** 接口,它要求音频数据必须是在编辑器时导入的资产,不过它里面很多算法结构可以供大家参考。 + +### MetaSound + +**Meta Sound** 是UE5推出的新一代音频系统,相较于旧版的 **Sound Cues** , **Meta Sound** 不仅具有更好的性能, 更重要的是它支持 **程序化 ( Procedural )** + +目前 Meta Sound 在 **UGC** 方面 展现出了非常大的潜力: + +- **Mix Universe** :[活动演讲 | 将音乐带入《Mix Universe》(官方字幕)](https://www.bilibili.com/video/BV1y84y1a7rr) +- **Fortnite** :[堡垒之夜 - AR 编曲](https://www.bilibili.com/video/BV1F94y1L7fW) + +如果你还不了解 Meta Sound,强烈建议去看这个基础视频: + +- [活动演讲 | 初学者的MetaSound指南(官方字幕)](https://www.bilibili.com/video/BV15G4y1A7B5) + +新建一个 **MetaSoundSource** ,创建如下的图表,点击播放,你能听到熟悉的正弦波: + +![image-20240205111218033](Resources/image-20240205111218033.png) + +Meta Sound 的图表是一种数据流图,这个图每帧都会执行,它的每个节点是一个功能函数块,每个节点会接受Input,并结合MetaSound执行的当前上下文,来填充Output数据,每一帧执行产生的音频会存储到MetaSoundSource 的 AudioBuffer 中,等待音频引擎播放。 + +> Trigger Pin 可以简单当成一个bool变量,输出节点会将它置为true,输入节点会读取它的值来判断该节点的逻辑是否要执行 + +以上面的正弦波为例,引擎在播放MetaSound时,会经过如下步骤: + +- 首先会创建构建MetaSound图表的异步任务,其对应的函数堆栈如下: + + ![image-20240205142028396](Resources/image-20240205142028396.png) + +- 在异步任务中,会加载所有节点(没有依赖的节点会被剔除),并根据依赖关系对剩下的节点进行拓扑排序,相应的代码堆栈如下: + + ![image-20240205143150293](Resources/image-20240205143150293.png) + +- 最终会通过节点内部的 **INodeOperatorFactory** 来创建 **IOperator** , **IOperator** 的部分定义如下,它提供了`BindInput`,`ResetFunction`,`ExecuteFunction`,`PostExecuteFunction`来编写相关逻辑: + + ``` c++ + class IOperator + { + public: + /** FResetOperatorParams holds the parameters provided to an IOperator's + * reset function. + */ + struct FResetParams + { + /** General operator settings for the graph. */ + const FOperatorSettings& OperatorSettings; + + /** Environment settings available. */ + const FMetasoundEnvironment& Environment; + }; + + virtual ~IOperator() {} + + /** BindInputs binds data references in the IOperator with the FInputVertexInterfaceData. + * + * All input data references should be bound to the InVertexData to support + * other MetaSound systems such as MetaSound BP API, Operator Caching, + * and live auditioning. + * + * Note: The virtual function IOPerator::BindInputs(...) will be made a + * pure virtual when IOperator::GetInputs() is removed at or after release 5.5 + * + * Note: Binding an data reference may update the which underlying object + * the reference points to. Any operator which caches pointers or values + * from data references must update their cached pointers in the call to + * BindInputs(...). Operators which do not cache the underlying pointer + * of a data reference do not need to update anything after BindInputs(...) + * + * Example: + * FMyOperator::FMyOperator(TDataReadReference InGain, TDataReadReference InAudioBuffer) + * : Gain(InGain) + * , AudioBuffer(InAudioBuffer) + * { + * MyBufferPtr = AudioBuffer->GetData(); + * } + * + * void FMyOperator::BindInputs(FInputVertexInterfaceData& InVertexData) + * { + * InVertexData.BindReadVertex("Gain", Gain); + * InVertexData.BindReadVertex("Audio", AudioBuffer); + * + * // Update MyBufferPtr in case AudioBuffer references a new FAudioBuffer. + * MyBufferPtr = AudioBuffer->GetData(); + * } + */ + virtual void METASOUNDGRAPHCORE_API BindInputs(FInputVertexInterfaceData& InVertexData); + + /** BindOutputs binds data references in the IOperator with the FOutputVertexInterfaceData. + * + * All output data references should be bound to the InVertexData to support + * other MetaSound systems such as MetaSound BP API, Operator Caching, + * and live auditioning. + * + * Note: The virtual function IOPerator::BindOutputs(...) will be made a + * pure virtual when IOperator::GetOutputs() is removed at or after release 5.5 + * + * Note: Binding an data reference may update the which underlying object + * the reference points to. Any operator which caches pointers or values + * from data references must update their cached pointers in the call to + * BindOutputs(...). Operators which do not cache the underlying pointer + * of a data reference do not need to update anything after BindOutputs(...) + * + * Example: + * FMyOperator::FMyOperator(TDataWriteReference InGain, TDataWriteReference InAudioBuffer) + * : Gain(InGain) + * , AudioBuffer(InAudioBuffer) + * { + * MyBufferPtr = AudioBuffer->GetData(); + * } + * + * void FMyOperator::BindOutputs(FOutputVertexInterfaceData& InVertexData) + * { + * InVertexData.BindReadVertex("Gain", Gain); + * InVertexData.BindReadVertex("Audio", AudioBuffer); + * + * // Update MyBufferPtr in case AudioBuffer references a new FAudioBuffer. + * MyBufferPtr = AudioBuffer->GetData(); + * } + */ + virtual void METASOUNDGRAPHCORE_API BindOutputs(FOutputVertexInterfaceData& InVertexData); + + /** Pointer to initialize function for an operator. + * + * @param IOperator* - The operator associated with the function pointer. + */ + using FResetFunction = void(*)(IOperator*, const FResetParams& InParams); + + /** Return the reset function to call during graph execution. + * + * The IOperator* argument to the FExecutionFunction will be the same IOperator instance + * which returned the execution function. + * + * nullptr return values are valid and signal an IOperator which does not need to be + * reset. + */ + virtual FResetFunction GetResetFunction() = 0; + + /** Pointer to execute function for an operator. + * + * @param IOperator* - The operator associated with the function pointer. + */ + using FExecuteFunction = void(*)(IOperator*); + + /** Return the execution function to call during graph execution. + * + * The IOperator* argument to the FExecutionFunction will be the same IOperator instance + * which returned the execution function. + * + * nullptr return values are valid and signal an IOperator which does not need to be + * executed. + */ + virtual FExecuteFunction GetExecuteFunction() = 0; + + /** Pointer to post execute function for an operator. + * + * @param IOperator* - The operator associated with the function pointer. + */ + using FPostExecuteFunction = void(*)(IOperator*); + + /** Return the FPostExecute function to call during graph post execution. + * + * The IOperator* argument to the FPostExecutionFunction will be the same IOperator instance + * which returned the execution function. + * + * nullptr return values are valid and signal an IOperator which does not need to be + * post executed. + */ + virtual FPostExecuteFunction GetPostExecuteFunction() = 0; + }; + ``` + + ![image-20240205142303554](Resources/image-20240205142303554.png) + +- 经过上面的流程后,图表已经构建完毕,还记得我们之前提到过,如果 **USoundWave** 的`bProcedural`为`true`,它会创建一个相应的 **ISoundGenerator** ,并在音频引擎的每一帧,调用`ISoundGenerator::OnGenerateAudio`来填充数据,没错, **Meta Sound** 正是这样的流程: + + ![image-20240205143955462](Resources/image-20240205143955462.png) + +- 来看看我们的正弦节点在这一帧执行时做了什么: + + > 每一帧播放,它会根据`DeltaPhase`递增`Phase`,然后从 **FSineWaveTableGenerator** 的`WaveTable`中获取到相关的样本值,填充到输出的AudioBuffer中 + + ![image-20240205150518808](Resources/image-20240205150518808.png) + +![image-20240205150739219](Resources/image-20240205150739219.png) + +至此,相信你已经基本搞懂了 **Meta Sound** 的基本原理。 + +但要想用好Meta Sound,还需要更多专业的音频知识,这方面笔者也是一个菜鸡,有点爱莫能助~ + +它里面提供了很多的函数节点,试一试,电脑又不会爆炸: + +![image-20240205152403078](Resources/image-20240205152403078.png) + +但有时候并不能完全满足我们的需求,现在,你可以试着做一些自定义的扩展。 + +#### 蓝图交互 + +**Meta Sound** 与蓝图并不相同,它支持的变量比较有限: + +![image-20240205152805417](Resources/image-20240205152805417.png) + +你同样也可以在C++中扩展这些变量,只需要找到合适的参考。 + +这些参数并没有直接暴露到属性编辑器,想要修改这些变量值有点复杂,我们可以借助 **UAudioComponent** 来简化这个过程,它派生自 **IAudioParameterControllerInterface** ,该接口提供了很多音频参数设置的便捷接口: + +![image-20240205154502428](Resources/image-20240205154502428.png) + +如果不想使用 **UAudioComponent** ,可以参考相关函数的具体实现。 + +这里需要特意提一下 UE 音频系统中的 `Trigger`,它与C++代码中常用的`Delegate`并不相同,我们可以简单把它当作一个bool变量,比如这样的图表: + +![image-20240205155420006](Resources/image-20240205155420006.png) + +已知每一个节点都会根据依赖关系的拓扑排序来执行,Input在执行时,会将输出的委托`播放时`置为true,在执行`AD Envelope`的时候,会读取输入委托`触发`与之相连的节点(即`播放时`),如果发现它为true才会执行该节点,在`AD Envelope`执行完毕后,会将输出委托`完成时`置为`true`,此时`Output`节点执行时检测到输入委托`完成时`为true,才会通知`MetaSoundSource`结束播放,也就是这里: + +![image-20240205155953096](Resources/image-20240205155953096.png) + +![image-20240205160115633](Resources/image-20240205160115633.png) + +#### 运行时构建 + +在 [Sounds Good](https://www.bilibili.com/video/BV1EC4y1J7Bs) 中,大佬提到了Meta Sound已经支持了运行时构建 Sound Graph,但是目前而言并不是很完善,还处于测试阶段,在 **MetasoundEngineTest** 中,能找到很多测试代码: + +![image-20240205160406177](Resources/image-20240205160406177.png) + +考虑到这部分代码后续可能会有较大的变动,因此暂不展开,后续官方版本稳定后再更新本节内容~ + +演示中还提到, Meta Sound 将推出大量新的音频控件!!!!!!!!!!!! + +![image-20240205161259486](Resources/image-20240205161259486.png) + + + diff --git "a/Docs/04-UnrealEngine/8.\345\274\200\346\224\276\344\270\226\347\225\214\346\212\200\346\234\257.md" "b/Docs/04-UnrealEngine/8.\345\274\200\346\224\276\344\270\226\347\225\214\346\212\200\346\234\257.md" new file mode 100644 index 00000000..54b6c81c --- /dev/null +++ "b/Docs/04-UnrealEngine/8.\345\274\200\346\224\276\344\270\226\347\225\214\346\212\200\346\234\257.md" @@ -0,0 +1,68 @@ +# 开放世界场景制作技术 + +- https://www.youtube.com/watch?v=Ur53sJdS8rQ + +![image-20240623091927982](Resources/image-20240623091927982.png) + +![image-20240623092017252](Resources/image-20240623092017252.png) + +![image-20240623092124043](Resources/image-20240623092124043.png) + +![image-20240623092259966](Resources/image-20240623092259966.png) + +![image-20240623092039005](Resources/image-20240623092039005.png) + +![image-20240623091909278](Resources/image-20240623091909278.png) + +![image-20240623092410845](Resources/image-20240623092410845.png) + +![-1](Resources/-1.gif) + +![image-20240623094904708](Resources/image-20240623094904708.png) + +![image-20240623095438717](Resources/image-20240623095438717.png) + +![image-20240623100042218](Resources/image-20240623100042218.png) + +![image-20240623100114909](Resources/image-20240623100114909.png) + +![image-20240623100423208](Resources/image-20240623100423208.png) + +- 开放世界质量验收: + - https://www.youtube.com/watch?v=2VDlX3Dqm0w + - https://www.youtube.com/watch?v=VdqhHKjepiE +- 项目管理: + - https://www.youtube.com/watch?v=M0uuDsjy4b0 + - https://www.youtube.com/watch?v=8m859pxcyLY +- 孤岛惊魂团队工作流:https://www.youtube.com/watch?v=HEaJfXVspPc + +https://www.youtube.com/watch?v=2VDlX3Dqm0w + +## 天空大气 + +## 地形 + +## 模型 + +动态环境 + +- 孤岛惊魂天气:https://www.youtube.com/watch?v=mGHCOOnI5aE +- 孤岛惊魂程序生成:https://www.youtube.com/watch?v=JBp8zvLVsgg + +- https://www.youtube.com/watch?v=JBp8zvLVsgg + +- https://www.youtube.com/watch?v=wavnKZNSYqU&t=2412s +- https://www.youtube.com/watch?v=gJKGMFcg29c&t=2211s +- https://www.youtube.com/watch?v=WMObgeULNTI +- https://www.youtube.com/watch?v=Ibe1JBF5i5Y +- 巫师3场景搭建:https://www.youtube.com/watch?v=p8CMYD_5gE8 + +![image-20240622181134887](Resources/image-20240622181134887.png) + +![image-20240622180634547](Resources/image-20240622180634547.png) + +![image-20240622180652772](Resources/image-20240622180652772.png) + +![image-20240623102606582](Resources/image-20240623102606582.png) + +![image-20240623102635905](Resources/image-20240623102635905.png) diff --git "a/Docs/04-UnrealEngine/8.\345\274\200\346\224\276\345\234\272\346\231\257\346\220\255\345\273\272.md" "b/Docs/04-UnrealEngine/8.\345\274\200\346\224\276\345\234\272\346\231\257\346\220\255\345\273\272.md" new file mode 100644 index 00000000..af35f957 --- /dev/null +++ "b/Docs/04-UnrealEngine/8.\345\274\200\346\224\276\345\234\272\346\231\257\346\220\255\345\273\272.md" @@ -0,0 +1,154 @@ +# 开放场景 + + + +- https://www.youtube.com/watch?v=Ur53sJdS8rQ + +![image-20240623091927982](Resources/image-20240623091927982.png) + +![image-20240623092017252](Resources/image-20240623092017252.png) + +![image-20240623092124043](Resources/image-20240623092124043.png) + +![image-20240623092259966](Resources/image-20240623092259966.png) + +![image-20240623092039005](Resources/image-20240623092039005.png) + +![image-20240623091909278](Resources/image-20240623091909278.png) + +![image-20240623092410845](Resources/image-20240623092410845.png) + +![-1](Resources/-1.gif) + +![image-20240623094904708](Resources/image-20240623094904708.png) + +![image-20240623095438717](Resources/image-20240623095438717.png) + +![image-20240623100042218](Resources/image-20240623100042218.png) + +![image-20240623100114909](Resources/image-20240623100114909.png) + +![image-20240623100423208](Resources/image-20240623100423208.png) + +- 开放世界质量验收: + - https://www.youtube.com/watch?v=2VDlX3Dqm0w + - https://www.youtube.com/watch?v=VdqhHKjepiE +- 项目管理: + - https://www.youtube.com/watch?v=M0uuDsjy4b0 + - https://www.youtube.com/watch?v=8m859pxcyLY +- 孤岛惊魂团队工作流:https://www.youtube.com/watch?v=HEaJfXVspPc + +https://www.youtube.com/watch?v=2VDlX3Dqm0w + +## 场景元素 + +### 天空大气 + +### 云 + +### 光影 + +### 地形 + +### 模型 + +- [认识游戏场景制作:游戏场景的作用及制作流程-网易游学-为热爱赋能 (163.com)](https://game.academy.163.com/course/careerArticle?course=495&isMaster=0) + + + +![微信图片_20240808105819](Resources/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20240808105819.png) + +![image-20240808105845345](Resources/image-20240808105845345.png) + +![image-20240808105838771](Resources/image-20240808105838771.png) + + + + + +### 植物 + +## 流送管理 + +## 动态效果 + +### 昼夜变换 + +### 四季变换 + +### 特效 + +### 风 + +### 水 + +### 火 + +### 雨 + +### 雪 + +### 雾 + +### 尘 + +### 电 + +### 光 + +[游戏场景美术构成要素 - 哔哩哔哩 (bilibili.com)](https://www.bilibili.com/read/cv12101296/) + +## 场景交互 + +### 水体交互 + +### 植被交互 + +[植被的动态交互---草 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/151342889) + +### 地面交互 + +### 物品交互 + +#### 破碎 + +#### 布料 + +## 场景管理 + +## 工程管理 + +## 项目管理 + +动态环境 + +- 孤岛惊魂天气:https://www.youtube.com/watch?v=mGHCOOnI5aE +- 孤岛惊魂程序生成:https://www.youtube.com/watch?v=JBp8zvLVsgg + +- https://www.youtube.com/watch?v=JBp8zvLVsgg + +- https://www.youtube.com/watch?v=wavnKZNSYqU&t=2412s +- https://www.youtube.com/watch?v=gJKGMFcg29c&t=2211s +- https://www.youtube.com/watch?v=WMObgeULNTI +- https://www.youtube.com/watch?v=Ibe1JBF5i5Y +- 巫师3场景搭建:https://www.youtube.com/watch?v=p8CMYD_5gE8 + +![image-20240622181134887](Resources/image-20240622181134887.png) + +![image-20240622180634547](Resources/image-20240622180634547.png) + +![image-20240622180652772](Resources/image-20240622180652772.png) + +![image-20240623102606582](Resources/image-20240623102606582.png) + +![image-20240623102635905](Resources/image-20240623102635905.png) + +- [优化UE5:重新思考高质量视觉效果的性能范式 - 第1部分:Nanite和Lumen |虚幻音乐节 2023 |Epic开发者社区 (epicgames.com)](https://dev.epicgames.com/community/learning/talks-and-demos/Vpv2/unreal-engine-optimizing-ue5-rethinking-performance-paradigms-for-high-quality-visuals-part-1-nanite-and-lumen-unreal-fest-2023) +- [Optimizing UE5: Rethinking Performance Paradigms for High-Quality Visuals - Pt 2: Supporting Systems | Unreal Fest 2023 | Epic Developer Community (epicgames.com)](https://dev.epicgames.com/community/learning/talks-and-demos/VlO2/unreal-engine-optimizing-ue5-rethinking-performance-paradigms-for-high-quality-visuals-pt-2-supporting-systems-unreal-fest-2023) +- [Vegetation Best Practices for UE5 | Epic Developer Community (epicgames.com)](https://dev.epicgames.com/community/learning/talks-and-demos/2lyj/unreal-engine-vegetation-best-practices-for-ue5) +- [Nanite Virtualized Geometry in Unreal Engine | Unreal Engine 5.4 Documentation | Epic Developer Community (epicgames.com)](https://dev.epicgames.com/documentation/en-us/unreal-engine/nanite-virtualized-geometry-in-unreal-engine) +- [Lumen Global Illumination and Reflections in Unreal Engine | Unreal Engine 5.4 Documentation | Epic Developer Community (epicgames.com)](https://dev.epicgames.com/documentation/en-us/unreal-engine/lumen-global-illumination-and-reflections-in-unreal-engine) + +- [离原春草 - 简书 (jianshu.com)](https://www.jianshu.com/u/e2b54829dd21) + +- [世界构建指南 (qq.com)](https://mp.weixin.qq.com/s/sbsDIN6dUtq5CGhqRBR5oQ) diff --git "a/Docs/04-UnrealEngine/9.\345\274\200\346\224\276\344\270\226\347\225\214\344\270\255\347\232\204\346\244\215\350\242\253\344\274\230\345\214\226\346\212\200\346\234\257.md" "b/Docs/04-UnrealEngine/9.\345\274\200\346\224\276\344\270\226\347\225\214\344\270\255\347\232\204\346\244\215\350\242\253\344\274\230\345\214\226\346\212\200\346\234\257.md" new file mode 100644 index 00000000..5055efd7 --- /dev/null +++ "b/Docs/04-UnrealEngine/9.\345\274\200\346\224\276\344\270\226\347\225\214\344\270\255\347\232\204\346\244\215\350\242\253\344\274\230\345\214\226\346\212\200\346\234\257.md" @@ -0,0 +1,442 @@ +# 开放世界中的植被优化技术 + +相信大家在一些大型游戏中,或多或少目睹过一些美轮美奂的自然风光,不得不惊叹这些游戏所展现出的艺术效果: + +![image-20240808134017142](Resources/image-20240808134017142.png) + +> 《对马岛之魂》 + +![image-20240808134539736](Resources/image-20240808134539736.png) + +> 《地平线2西之绝境》 + +![image-20240808134944231](Resources/image-20240808134944231.png) + +> 《古墓丽影:暗影》 + +![image-20240808135113053](Resources/image-20240808135113053.png) + +> 《柯娜精神之桥》 + +![image-20240808141433678](Resources/image-20240808141433678.png) + +> 《燕云十六声》 + +![image-20240808135612492](Resources/image-20240808135612492.png) + +> 《刺客信条:奥德赛》 + +而这些游戏,都不是UE做的... + +需要承认的是,尽管 Unreal Engine 5 非常强大,但也并非无所不能,想要搭建出像上面那样能够满足游戏项目性能要求的自然场景,我们还将会面临许多除引擎技术之外的挑战,在开放的自然场景中,植被产生性能问题的频率往往不亚于光影和特效。 + +在UE5中,如果谈到植被优化,大家的第一反应想到的可能是减面,实例化,距离裁剪,优化材质,使用LOD或者Nanite... + +这些资产级别的调优和复杂的特化手段,往往很难支撑起开放场景中茂盛的植被生态。 + +因此,本文会阐述一些关于植被优化的策略和观点。 + +## 植物资产制作 + +大多数的三维建模软件都能制作植物资产,但在游戏行业中, **[Speed Tree](https://store.speedtree.com/)** 是主流的植被制作工具。 + +![image-20240808182351274](Resources/image-20240808182351274.png) + +与岩石,物品,建筑等常见模型不同的是,植物往往具有非常多的茎和叶,在几何层面,意味着植物的模型通常拥有大量密集的顶点和面片,这会带来非常高额的渲染开销,这里有一个示例: + +![image-20240808193318835](Resources/image-20240808193318835.png) + +- 这样一棵并不是那么茂密的树,却具有高达两百万的三角形,这庞大的顶点数据量和大量的重复绘制够GPU好好喝一壶了。 + +这种使用模型的几何来表示植物结构的做法我们一般称为 **模型树/草** ,通常这种规格的资产是无法大量用于游戏项目的。 + +而现有的游戏项目中更多的是使用 **插片/遮罩** 的方式来制作植物资产,通过将一部分的枝叶压到一张纹理面片上,通过在树枝上大量插片来展现出枝繁叶茂的景象,这是一种非常取巧的制作方式,虽然在效果上存在一些瑕疵,但它带来的性能收益是非常客观的。 + +![image-20240808195330576](Resources/image-20240808195330576.png) + +插片的方式曾一直是游戏行业公认的植被渲染最高效的方式,但它也存在一些缺陷: + +- 存在视觉缺陷,近镜头容易穿帮。 +- 使用遮罩材质,虽然顶点数量有所减少,但依旧存在大量的OverDraw,且阴影的绘制消耗更高。 +- 植物模型缺少枝叶的顶点特征,全局光照效果有所损失。 + +随着 UE5 虚拟几何体技术 - Nanite 的出现,让这一局势发生了一些变化。 + +Nanite的优势在于使用GPU驱动的管线,可以快速切换几何的细节,并及时剔除不可见的三角形,在尽可能不影响视觉效果的前提下,极大程度地提高顶点利用率(占屏比)。 + +![image-20240808202239795](Resources/image-20240808202239795.png) + +> 前文所用的模型树,图片只有一百万的像素,但顶点却有两百万,很显然大部分顶点对画面效果的贡献并不高。 + +关于Nanite,这里有一个很好的官方使用指南: + +- [文档 | Nanite虚拟几何体 (epicgames.com)](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/nanite-virtualized-geometry-in-unreal-engine) +- [[官方培训]23-为UE创建美术资产 | Epic 李文磊](https://www.bilibili.com/video/BV18X4y1k7R1/) + +对于Nanite植被,官方也做了更细致的对比: + +- [Vegetation Best Practices for UE5 | Epic Developer Community (epicgames.com)](https://dev.epicgames.com/community/learning/talks-and-demos/2lyj/unreal-engine-vegetation-best-practices-for-ue5) + +![image-20240808203040303](Resources/image-20240808203040303.png) + +![image-20240808203242532](Resources/image-20240808203242532.png) + +在官方的测试报告中,在同样开启Nanite的前提下,展现出了与传统渲染方式截然不同的结果 —— 使用模型树的性能更优于插片树。 + +根据这一分析结果,是否意味着我们可以借助Nanite采用模型树的方式来更高效的渲染植被? + +答案是否定的。 + +> 一条有效论据是:前文列出的那些游戏的截图画面,它们并没有使用像Nanite这样的虚拟几何体技术,但也搭建出了惊艳的自然生态风光,反观... + +### Nanite 之殇 + +否定的原因并非是在意顶点数据量和显存占用,因为植被在场景中,通常以实例化的方式进行绘制,尤其是开放世界场景中,我们非常在意分块之间性能的负载均衡,虽然采用Nanite的 **单个** 模型树 可能确实具有更好的性能表现,但这也会阻碍我们搭建出性能优越的统一植被策略。 + +下面会分几个方面来阐述一下使用Nanite存在的问题,观点可能有些主观,有不对的地方,烦请大佬帮忙指正~ + +- 更高的效果预期伴随着更难把控的性能平衡 +- 不友好的实例化 +- 并非最具性价比的优化方式 + +#### 更高的效果预期伴随着更难把控的性能平衡 + +UE5 发布演示的时候,相信很多小伙伴都被演示所展现出的画面效果所震撼,当时很多人可能都在想,或许我们即将迎来下一个世代的游戏画面: + +![image-20240809095412837](Resources/image-20240809095412837.png) + +不管你们信不信,反正我是信了(狗头),我所了解到的很多团队,也都信了。 + +信了的人,这几年作为官方小白鼠,可以说是吃了不少苦头,这对一个商业项目来说,更是有着不容小觑的影响。 + +但吐槽归吐槽,Nanite确实是一项突破性的技术,相比传统的LOD有着绝对的优势,随着这几年官方的优化迭代,其可靠性也逐渐提升。 + +目前它的的确确可以被广泛应用于实际的游戏项目中,但Nanite所带来的问题,通常不在于其自身。 + +:我都用Nanite了,做个百万三角形的小石头不过分吧,反正会自动减面。 + +:你这么点顶点也叫树?细节都没了,看不起谁呢? + +这种情况通常会出现在使用UE的新兴团队当中,而实际上,UE的爆火距离现在也并不久远,使用UE的团队大多也没有深厚的引擎技术积累(国内更甚),对于这种新兴的技术,这样的团队会容易盲从一些营销上的话术,从而弱化甚至忽视内容制作管线上的迭代和积累。 + +正因为有了Nanite的加持,通常美术制作人员会对游戏画面有着更高的要求,在优化层面来看,虽然Nanite可以提高顶点的利用率,但如果使用了细节更多的源资产,将意味着更大的磁盘占用,整个场景画面也会有着相比以往更多更密集的顶点。 + +其中顶点的数量将严重影响到一些渲染阶段的性能消耗: + +- Shadow Map 的构建 +- 速度缓冲区的绘制 + +而磁盘占用不仅会影响到最终的包体大小,更重要的是会大幅增加项目场景管理的难度。 + +以对马岛之魂为例,大家可能觉得它的地图并没有特别大(28.62 km2),模型种类少,精度也不高,但它的包体却有50+GB,更加令人恐慌的是,它的开发时资产大小: + +![image-20240623091927982](Resources/image-20240623091927982.png) + +笔者非常建议去看一下这场GDC,对大型项目的资产管理有一个概念: + +- [Zen of Streaming: Building and Loading 'Ghost of Tsushima' (youtube.com)](https://www.youtube.com/watch?v=Ur53sJdS8rQ&t=794s) + +试想一下,我们如果用UE的Nanite去复刻对马岛之魂那样的场景,保守一些,我们把资产精度提升个四五倍,可以预想到这么庞大的项目资产所带来的管理成本会有多高。 + +因此,如果是游戏项目,笔者还是建议请尽可能管理好对场景美术效果的预期,它不应该影响甚至拖累游戏性的迭代。 + +> 如果游戏场景在开发机上都不能够稳定运行80帧,并且严重侵占玩法或其他模块的性能预算和迭代空间,就是搭得再好看,缺乏游戏性,那又有什么用呢? +> 笔者相信绝大部分玩家都不会因为一个游戏仅仅拥有照片级的写实画面就傻傻的买单。 + +#### 不友好的实例化 + +笔者对Nanite的认知经历了几个阶段: + +1. “应用尽用”:一言不合开Nanite。 +2. 遇到坑了,草不应该用Nanite,这种密集实例化渲染的物体开启Nanite会有一些Bug。 +3. 又遇到坑了,Nanite的特性不能贴合HISM机制,理论上要用HISM渲染的物体,就不能开Nanite。 +4. 在深入了解一些项目的制作手段之后 —— 正经人谁用Nanite做植被哦。 + +先说第一个坑,Nanite的减面是对单个实例友好的,它可以轻易将一个数百万三角形的模型减到变为空像素最后被裁剪掉,但像草这种通过实例化动辄数万个实例的情况,使用Nanite的退化并不能有效减少实例,这会很容易给Nanite的调度计算产生巨大的开销,当前画面里的Nanite的实例数量超过了一定阈值时,会出现画面异常,虽然有一些引擎指令有助于改善该现象。 + +而第二个坑,要结合世界分区的HLOD来谈,HLOD是一种在开放世界中用来显示在流送加载范围之外的 **远景代理** ,平常的模型可以在分区中 **Merge** + **Remesh(重构)** 来生成远景HLOD,但对于植被而言,对这种零碎面片比较多的物体使用体素化的Remesh算法会生成棉花糖, **Merge** + **Reduce(减面)** 的方式并不能在大幅度减少顶点数量和DrawCall的前提下,保障远景植被的画面质量,因此植被的HLOD,只能使用 **实例化(Instancing)** ,而世界分区中的实例化优化,除了对实例化植被进行 **区域切块** ,还需要用上 **层级实例化静态网格 (HISM :HierachicalInstancedStaticMesh)** ,与 **实例化静态网格(ISM :InstancedStaticMesh)** 不同的是,层级实例化网格允许在实例化网格内部切换LOD,比如下面的所有树木都是用一个HISM绘制的: + +![image-20240809135411348](Resources/image-20240809135411348.png) + +HISM本质上是将实例化网格划分成ClusterTree,每一个Cluster有其独立的LOD级别,最终会产生多个级别的ISM,通过一些引擎指令,我们可以管控HISM的划分策略,关于具体的实现细节可以参考: + +- [UHierarchicalInstancedStaticMesh(HISM)原理分析-CSDN博客](https://blog.csdn.net/qq_29523119/article/details/123932029) +- [HISM 大规模植被渲染解决方案-CSDN博客](https://blog.csdn.net/ZJU_fish1996/article/details/108806360) + +而Nanite不像传统的LOD方案那样,具有少数固定精度梯度的模型,它的LOD梯度不是模型级别的,而是簇(Cluster)级别的,因此HISM机制不能有效地和Nanite一起正常正常工作,这也是为什么我们不使用Nanite来制作大型植被的真正原因。 + +#### 并非最具性价比的优化策略 + +通常优化模型的手段有两种: + +- **减面(Reduce)** :在源模型的几何数据上进行删减从而得到简化的模型。 +- **重构(Remesh)** :通过体素化的模型算法,以一定体素精度来逼近源模型从而得到简化的模型。 + +> Nanite 使用的是减面的算法 + +这两种策略有着不同的适用场景: + +- 减面:适用于面片,它通常用于对几何棱角追求比较高的模型,如地形,建筑,植物,水体面片... +- 重构:适合模型闭合,几何形体比较圆润的模型,它通常能以极少的顶点来保证源模型的几何形态,如石头,建筑,道具... + +对于树而言,假如我们采用这两种策略来生成相同三角形数量的简模: + +![image-20240809151946858](Resources/image-20240809151946858.png) + +这两种策略都存在各自的问题: + +- 重构:模型变得蓬松,UV映射异常的地方出现黑块。(实际上要生成效果良好的重构模型,体素算法的参数得做很多微调和特化) +- 减面:丢失大量树叶细节。 + +在这方面,笔者做过很多尝试,可以明确的说,通过自动化的模型处理算法,很难得到一个效果优良的植物简模。 + +想要达到好的简模效果,可以在植被制作时,手动通过剪枝的方式来制作。 + +如果嫌手动制作的工作量过高,可以通过基于植物顶点色权重的减面方式。 + +> 通常情况下是需要刷顶点色作为植物风力计算时顶点偏移幅度的权重,可以根据这一权重因子,来作为区域简化幅度的权重。 + +减面的方式可以用来生成植物模型的几个较高精度的LOD,但可惜的是,这样生成的简模往往仍然具有较高的顶点数量(上面简化到1500+个顶点的时候,模型已经面目全非了),想要让顶点数量进一步大幅减少,但却要保证模型的表现效果,很显然,使用简化的方式是做不到的,我们必须得打破原有模型的几何结构,而 Remesh 的方式,往往会让一棵树变得非常蓬松,并且有UV映射异常的问题,它的表现效果并不好,那有没有其他手段能解决这个问题呢? + +答案是有的,我们最终的目的是保证模型的显示效果,而非几何结构,这也引申出了模型优化的另一种策略 —— **替身(Impostor)** 。 + +> 替身 是笔者自己瞎翻译的,译作 代理 容易跟 Proxy 混淆, 译作 冒名顶替者 又不太好听。 + +我们一般会用替身去作为植被模型的最后一级LOD,比较常见的替身策略有以下几种: + +- **公告牌(Billboard)** :将某个特定角度的模型绘制到纹理上。 +- **翻页牌(Flipbook)** :将多个角度的模型绘制到纹理上,最后在材质上根据玩家视角来切换显示的纹理。 +- **卡片簇(Billboard Cloud/ Mesh Card Cloud):** 将模型绘制在许多的网格插片上,从而来堆叠出源模型相似的视角效果。 +- **八面体(Octahedron)** :将一定视角分布的模型绘制到纹理上,最后在材质上插值多个角度的图像来呈现出可靠的视角过渡效果。 + +![preview](Resources/v2-ac82f9df3eb6d0ab62edc448957c80e6_r.jpg) + +- [游戏场景植被优化 —— Impostor - Italink的视频 - 知乎](https://www.zhihu.com/zvideo/1790733617217019905) + +这些替身策略的实现原理并不复杂,清楚它们的原理之后我们也能轻易搭建出一条替身生成管线,不过有一些专门的SDK可以供我们使用: + +- [Simplygon - 树立 3D 游戏内容优化标准](https://www.simplygon.com/) +- [InstaLOD - 生产和自动优化3D内容所需的一切](https://instalod.com/zh/) + +笔者测试时使用的是 InstaLOD(个人使用免费),这是它提供的UE插件,里面包含了诸多的模型处理算法: + +![image-20240809161248634](Resources/image-20240809161248634.png) + +对于 **八面体(Octahedron)** ,UE提供了专门的插件: + +![image-20240809161621579](Resources/image-20240809161621579.png) + +关于其使用方式,可以查看这篇文字: + +- [【性能优化】impostor在虚幻中使用(植被优化) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/693799380) + +> 这对一个实际项目来说最有价值的并非是编辑器,而是里面的模型处理相关的代码接口有助于项目团队搭建出符合自身项目规格的模型生成管线。 + +这么多的替身策略,有小伙伴可能就会问了, 谁的性能效果更好呢? + +Billboard 和 Flipbook 有非常少的顶点,它们的性能效果无疑是最好的,但它们的表现效果往往不佳。 + +比较有争议的主要是 卡片簇 和 八面体 的方式,在之前的很多3A项目中,所采用的基本都是卡片簇的方式,而笔者在 UE 论坛中发现,其实很多人更推荐使用八面体。 + +笔者这里做了一个简单的对比测试,具体谁优谁劣可能需要更多的数据支撑: + +**源模型** + +- 三角形数量:639639 +- 贴图尺寸:4096 +- GPU耗时: 15.52ms + +![微信图片_20240808105819](Resources/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20240808105819.png) + + **卡片簇** + +- 三角形数量: 32 +- 贴图尺寸:2048 +- GPU耗时:10.81ms + +![image-20240808105845345](Resources/image-20240808105845345.png) + +**八面体** + +- 三角形数量: 32 +- 贴图尺寸:2048 +- GPU耗时:7.75ms + +![image-20240808105838771](Resources/image-20240808105838771.png) + +可以明显看出八面体的性能和画面表现更优于卡片簇的方式,但如果只是作为最后一级LOD的话,卡片簇的纹理尺寸还有很大的优化空间,具体采用什么方案可能就要仁者见仁智者见智了。 + +### 制作参考 + +对于LOD的制作标准,可以参考地平线在GDC上的分享,只不过最后一级LOD变成了八面体: + +- [Between Tech and Art: The Vegetation of Horizon Zero Dawn (youtube.com)](https://www.youtube.com/watch?v=wavnKZNSYqU&t=2412s) +- [Interactive Wind and Vegetation in 'God of War' (youtube.com)](https://www.youtube.com/watch?v=MKX45_riWQA) + +> 如果要保证植被的LOD过渡没有明显异常,需要在制作时,以较少的主枝干尽可能营造出更大的覆盖范围,用副枝干去增添植被的细节,制作低级别的LOD时,只需要减少副枝干的数量即可。 + +![image-20240622181134887](Resources/image-20240622181134887.png) + +![image-20240622180652772](Resources/image-20240622180652772.png) + +![image-20240622180634547](Resources/image-20240622180634547.png) + +### 总结 + +上面说了很多琐碎的信息,这里统一总结一下: + +- 相同茂密程度的植物,使用模型树的方式会产生大量的顶点,它所带来的性能和内存消耗远高于插片树的方式。 +- 无论是模型树还是插片树,开启Nanite是一定有优化提升的,但Nanite 不支持 HISM,所以如果项目场景中有大量的植被,就不建议使用Nanite植被,而是通过传统的LOD制作流程借助ISM和HISM进行优化。 +- 对于新兴团队来说,大家都是摸着石头过河,很难说什么方案是最优的,定好之后按这个规范铺量就高枕无忧了。面对这种解决方案不定的问题,我们最好的管理方案就是搭建好自己的内容生成管线,来尽可能地去减少迭代和验证过程中的沉没成本。 + +## 植被组织 + +在确定了植被制作管线之后,还需要考虑植被在场景中的规划方式。 + +它不像其他常规静态模型一样,在开放世界场景中 Merge+Remesh 就能生成一个高品质的远景代理,但植被不行,它不能被Merge成一个Mesh,或者说Merge成一个Mesh之后,没有什么很好的策略可以既保证植被的效果,又能大幅度的削减它的顶点和优化它的材质,因此如果要制作大量的植被的话,只能采用实例化的方式来铺设,所以在美术效果上,一定是会有一些妥协的。 + +### 搭建方式 + +在UE中创建大量植被,主要有以下几种方式: + +- **植被编辑模式(Foliage EdMode)** + + ![image-20240809175349308](Resources/image-20240809175349308.png) + +- **地形草地类型(Landscape Foliage Type)** + + ![image-20240809175526774](Resources/image-20240809175526774.png) + +- **程序内容生成框架** ![image-20240809175559308](Resources/image-20240809175559308.png) + +这几种方式有各自的使用场景: + +- 植被编辑模式:对于一个正经点的游戏项目来说,我只能说 **植被编辑模式 是碰都不能碰的东西** ,这种刷植被的方式,返工成本巨高,也很难做跨平台的性能适配。 + +- PCG:PCG框架毫无疑问是最优的植被编辑方式,使用它可以轻易快速的搭建出生态群落,并且可以定制一些的可延展性参数来做多平台的适配。 +- 地形草地类型:它适用于密集草的绘制,UE中的实现应该是参考了对马岛中[程序化草](https://www.bilibili.com/video/BV1SF411N7RW/)的管理流程,在预览时通过异步任务去生成草簇,它虽然很高效,但使用地形草地类型会占用一个地形图层去描述草的密度,并不适合大规模使用。 + +### 分区与流送 + +首先再回顾一下关于UE5世界分区的内容,之前笔者整理过一个相关文章: + +- [Unreal Engine 5 开发 — 开放世界制作 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/670363215) + +前段时间官方也给出了一些比较关键的构建指南: + +- [世界构建指南 (qq.com)](https://mp.weixin.qq.com/s/sbsDIN6dUtq5CGhqRBR5oQ) +- [Advanced World Building in Unreal Engine - Chris Murphy / Epic Games for WA Games Week 2023 - YouTube](https://www.youtube.com/watch?v=gJKGMFcg29c&t=2211s) + +相信大家对 **流送(Streaming)** 也有了一定概念: + +![image-20240809170000187](Resources/image-20240809170000187.png) + +这是笔者在个人测试项目中所使用的网格配置: + +| GirdName | Cell Size | Loading Range | Priority | BlockOnSlowStreaming | +| ------------------- | --------- | ------------- | -------- | -------------------- | +| MainGrid(Default) | 12800 | 25600 | 0 | false | +| DeferGrid | 25600 | 25600 | -9 | true | +| AdvanceGrid | 25600 | 51200 | 9 | true | +| SmallGrid | 6400 | 12800 | -9 | false | + +![image-20240809171548496](Resources/image-20240809171548496.png) + +通常 256 m 的加载范围可以保证视野范围内的绝大部分表现效果是正常的,下面是3D视图下的效果(远处的角色是256m下的小黑点): + +![image-20240809171835592](Resources/image-20240809171835592.png) + +场景模型一般是放在 `Main Gird` 中,但考虑到单元格尺寸(CellSize)也会影响到HLOD合批的距离大小,所以笔者推荐把植被放到 `DeferGrid`中 + +通常植被在近距离使用的是HISM的方式进行组织,这就要求在项目场景搭建的时候,需要严格管控植被的种类数量,越少越好,在超出加载范围之后: + +- 对于小型植被,可以直接不显示远景代理,如果用的是地形草,通常需要保证草的颜色跟地形颜色基本一致,这样在远距离时才不会有特别明显的视觉瑕疵,如果不是使用地形草,使用的是程序化草,可以在编辑器下,通过程序化蓝图中从顶视图烘培一张颜色纹理,以贴花的方式进行渲染来作为草的远景代理。 +- 对于大型植被,通常指树木,可以在超出加载范围之后,可以将HISM退化为使用最低级别LOD的ISM作为HLOD(HISM具有簇划分和LOD切换调度的开销),就像是这样: + +![image-20240809173840266](Resources/image-20240809173840266.png) + +> 植被的LOD需要能够平滑过渡,由于超出加载范围时HISM会变为ISM,需要保证加载距离左侧和右侧的LOD等级是一致的,这也就意味着模型的LOD梯度是分布在加载距离之内的,由于UE目前使用屏幕空间大小作为模型LOD的切换依据,因此在制作时,需要将其转化为对应的距离。 + +虽然有了世界分区的加持,并不意味着我们可以把场景搭的无限大,它的优势更多的是无缝加载,计算机的实时计算资源是有限的,虽然可以通过这套合批机制进一步扩大距离,但越大的范围所带来的性能开销也就越大,留给玩法的预算也就越少,因此,角色能看到的距离应该依旧维持在一定距离之内,比如768m: + +![image-20240809174827247](Resources/image-20240809174827247.png) + +> 不只是树木,其他如模型,粒子,动态材质效果等也建议遵守该规则。 + +## 阴影优化 + +植被的阴影消耗往往是整个场景中占比非常大的一环,因为它具有大量密集且重叠的三角形。 + +那怎么优化阴影呢? + +首先,我们需要思考一个问题,为什么需要阴影?假如我们把阴影关了,那么将会看到: + +![image-20240809193856172](Resources/image-20240809193856172.png) + +如果让玩家看完上面的对比画面之后,提问:你是喜欢有阴影的画面呢,还是喜欢没阴影的画面? + +那玩家的回答一定是有阴影的。 + +进一步提问:为什么喜欢阴影呢? + +大多数的回答可能是:没阴影的话,画面看起来很违和。 + +确实,当我们把阴影关了之后,画面的空间层次关系变得差了很多,这已经违反了人对空间的直观感受。 + +在游戏开发的过程中,我们要学着揣摩玩家的真正的意图,就像在这里一样,本质上玩家在意的并非是想要阴影,他们可能都不在乎什么是阴影?他们仅仅只是不喜欢没有阴影之后违和的画面,而已。 + +笔者认为,通常在游戏项目中,追求物理或学术上的正确通常是没有意义的,或者说它的性价比并不高,我们只要保证玩家深层次的需求,以一种性能优越的方式, 能充分表现出画面的层次感其实就够了。 + +看着这么扁平的画面: + +![image-20240809195258560](Resources/image-20240809195258560.png) + +想要增加它的层次感,你能想到什么? + +SSAO 呀! + +> 使用 Lumen 后默认不开启 SSAO,可以使用如下指令开启: +> +> `r.Lumen.ScreenProbeGather.ShortRangeAO 0` +> +> `r.Lumen.DiffuseIndirect.SSAO 1` + +![image-20240809195940083](Resources/image-20240809195940083.png) + +- 如果可以把AO对画面的权重收益提高一些,画面的层次感就会好很多,但这种想法跟目前的Lumen相悖,感兴趣的小伙伴可以试下。 + +提升AO只能增加画面中细微的空间感,如果没有大面积的空间阴影,画面看起来还是可能会比较奇怪。 + +对于草,我们可以直接把草的动态阴影给关闭,把接触阴影打开,接触阴影可以看成是一种基于屏幕空间的阴影算法,它几乎是为草量身定制的阴影策略: + +> 在模型组件开启接触阴影后,可以在定向光源中设置接触阴影的长度。 + +![image-20240809201526488](Resources/image-20240809201526488.png) + +- 从上面的对比图不难看出,接触阴影的效果已经很逼近动态阴影了,而它的性能损耗却非常低。 + +对于树而言,它不能像草一样使用屏幕空间一样的阴影算法,但我们可以优化提交给阴影的顶点数据,因为我们采用的LOD的方式来渲染树木,假如画面中显示的是LOD0,我们完全可以不用LOD0来绘制阴影,而是使用LOD2,或者LOD3... + +在UE中,我们可以通过引擎指令`r.ForceLODShadow`,来锁定构建ShadowMap所使用的LOD: + +![image-20240809203942983](Resources/image-20240809203942983.png) + +> 通过对比图我们可以看出使用不同LOD的阴影存在一些细微差异,但如果不说的话,谁知道呢? + +## 碰撞及交互优化 + +对于树的碰撞制作,具体要看项目对场景的要求,在熟悉前文的一些网格处理算法之后,我们也可以很容易搭建出一条树木碰撞体的生成管线,比如借助Remesh蓬松的特性可以轻易生成一个像棉花糖一样少量顶点的碰撞体,或者说根据顶点色的分布,只生成树干部分的碰撞... + +而对于交互而言,由于我们近距离使用的HISM,并非是一个真实的个体,通常是不能正常交互的。 + +如果我们需要交互的话,就需要对HISM做一些特殊处理,比如引擎里面扩展了 **UFoliageInstancedStaticMeshComponent** ,它可以用来处理实例的伤害计算: + +![image-20240809223452013](Resources/image-20240809223452013.png) + +如果有需求的话,也可以自己派生HISM,让近距离的实例退化为一个实际的Actor,这里笔者不建议走世界分区的方式来管理这种退化。 + +## 其他 + +草皮里面过多的顶点会增加速度缓冲区的绘制消耗,UE也扩展了一个 **UGrassInstancedStaticMeshComponent** 允许我们关闭 HISM 的 WPO 速度写入: + +![image-20240809224217321](Resources/image-20240809224217321.png) diff --git a/Docs/04-UnrealEngine/Docs.pptx b/Docs/04-UnrealEngine/Docs.pptx new file mode 100644 index 00000000..8aa7283e Binary files /dev/null and b/Docs/04-UnrealEngine/Docs.pptx differ diff --git "a/Docs/04-UnrealEngine/FX/0.\344\274\240\351\200\201\351\227\250\346\225\210\346\236\234.md" "b/Docs/04-UnrealEngine/FX/0.\344\274\240\351\200\201\351\227\250\346\225\210\346\236\234.md" new file mode 100644 index 00000000..45f8662c --- /dev/null +++ "b/Docs/04-UnrealEngine/FX/0.\344\274\240\351\200\201\351\227\250\346\225\210\346\236\234.md" @@ -0,0 +1,66 @@ +--- +comments: true +--- + +# 传送门效果 + +## 演示 + +![transfoa12we](Resources/transfoa12we.gif) + +## 原理 + +- 制作一个入口点和出口点蓝图Actor,出口点蓝图带有一个 `场景捕获2D组件`,保证其视角参数跟玩家相机参数完全一致,并根据玩家相机与入口点相对位置,同步`场景捕获2D组件`和出口点的相对位置,就像这样(红色小方块对应场景捕获组件): + + ![transfoa12w122121e](Resources/transfoa12w122121e.gif) + +- 将捕获得到的RT结合Screen UV显示在入口平面上 + +## 资产 + +![image-20240710114119159](Resources/image-20240710114119159.png) + +### M_Transfer + +![image-20240710114314595](Resources/image-20240710114314595.png) + +### BPA_TransferExit + +变量 : + +- RenderTarget:类型为 **纹理渲染目标2D** , 存储捕获到的RT + +![image-20240710114541763](Resources/image-20240710114541763.png) + +### BPA_TransferEntry + +变量 : + +- Exit:类型为 **BPA_TransferExit** ,注意勾选可编辑实例 +- SceneColor:类型为 **纹理渲染目标2D** , 存储从出口蓝图获取到的RT + +同步捕获组件位置: + +![image-20240710114743230](Resources/image-20240710114743230.png) + +确保出口蓝图的RT创建完毕,并为入口蓝图的平板创建动态材质,传递纹理参数: + +![image-20240710114942746](Resources/image-20240710114942746.png) + +传送逻辑: + +![image-20240710115048248](Resources/image-20240710115048248.png) + +## 使用 + +在场景中分别放置 `BPA_TransferEntry` 和 `BPA_TransferExit`,并在 BPA_TransferEntry 的细节面板中指定对应的 出口蓝图,就能正常使用了。 + + ## 注意事项 + +- 场景捕获没有抗锯齿。 +- 部分光影功能是基于玩家视角的,出口处的画面可能存在一些异常。 +- 还需要自行组织关卡的流送逻辑 +- 还需要优化视角同步和绘制的生命周期 +- 玩家相机组件通常会设置 `宽高比约束`,通常是跟随项目设置`维持X轴视野`,但场景捕获组件内部默认设置为`维持主轴视野`,这个参数不一致会导致画面不同步,场景组件没有把这个参数暴露出来,同步的话需要自己派生进行覆写。 + +![image-20240710120041199](Resources/image-20240710120041199.png) diff --git a/Docs/04-UnrealEngine/FX/Resources/image-20240710114119159.png b/Docs/04-UnrealEngine/FX/Resources/image-20240710114119159.png new file mode 100644 index 00000000..a6e1cb8e Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/image-20240710114119159.png differ diff --git a/Docs/04-UnrealEngine/FX/Resources/image-20240710114314595.png b/Docs/04-UnrealEngine/FX/Resources/image-20240710114314595.png new file mode 100644 index 00000000..7ba213d8 Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/image-20240710114314595.png differ diff --git a/Docs/04-UnrealEngine/FX/Resources/image-20240710114541763.png b/Docs/04-UnrealEngine/FX/Resources/image-20240710114541763.png new file mode 100644 index 00000000..00346df5 Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/image-20240710114541763.png differ diff --git a/Docs/04-UnrealEngine/FX/Resources/image-20240710114743230.png b/Docs/04-UnrealEngine/FX/Resources/image-20240710114743230.png new file mode 100644 index 00000000..e8e223bb Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/image-20240710114743230.png differ diff --git a/Docs/04-UnrealEngine/FX/Resources/image-20240710114942746.png b/Docs/04-UnrealEngine/FX/Resources/image-20240710114942746.png new file mode 100644 index 00000000..3b25d3b0 Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/image-20240710114942746.png differ diff --git a/Docs/04-UnrealEngine/FX/Resources/image-20240710115048248.png b/Docs/04-UnrealEngine/FX/Resources/image-20240710115048248.png new file mode 100644 index 00000000..8dae6b23 Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/image-20240710115048248.png differ diff --git a/Docs/04-UnrealEngine/FX/Resources/image-20240710120041199.png b/Docs/04-UnrealEngine/FX/Resources/image-20240710120041199.png new file mode 100644 index 00000000..ee6efe10 Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/image-20240710120041199.png differ diff --git a/Docs/04-UnrealEngine/FX/Resources/transfoa12w122121e.gif b/Docs/04-UnrealEngine/FX/Resources/transfoa12w122121e.gif new file mode 100644 index 00000000..91123454 Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/transfoa12w122121e.gif differ diff --git a/Docs/04-UnrealEngine/FX/Resources/transfoa12we.gif b/Docs/04-UnrealEngine/FX/Resources/transfoa12we.gif new file mode 100644 index 00000000..7c1403a1 Binary files /dev/null and b/Docs/04-UnrealEngine/FX/Resources/transfoa12we.gif differ diff --git a/Docs/04-UnrealEngine/Resources/-1.gif b/Docs/04-UnrealEngine/Resources/-1.gif new file mode 100644 index 00000000..a868f94b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/-1.gif differ diff --git a/Docs/04-UnrealEngine/Resources/12312.gif b/Docs/04-UnrealEngine/Resources/12312.gif new file mode 100644 index 00000000..822653b3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/12312.gif differ diff --git a/Docs/04-UnrealEngine/Resources/123123176812.gif b/Docs/04-UnrealEngine/Resources/123123176812.gif new file mode 100644 index 00000000..232362c3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/123123176812.gif differ diff --git a/Docs/04-UnrealEngine/Resources/123176812.gif b/Docs/04-UnrealEngine/Resources/123176812.gif new file mode 100644 index 00000000..12536c87 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/123176812.gif differ diff --git a/Docs/04-UnrealEngine/Resources/1232265272.gif b/Docs/04-UnrealEngine/Resources/1232265272.gif new file mode 100644 index 00000000..52710560 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/1232265272.gif differ diff --git a/Docs/04-UnrealEngine/Resources/12332312.gif b/Docs/04-UnrealEngine/Resources/12332312.gif new file mode 100644 index 00000000..8ac063d5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/12332312.gif differ diff --git a/Docs/04-UnrealEngine/Resources/1236572.gif b/Docs/04-UnrealEngine/Resources/1236572.gif new file mode 100644 index 00000000..9f5adbca Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/1236572.gif differ diff --git a/Docs/04-UnrealEngine/Resources/123812.gif b/Docs/04-UnrealEngine/Resources/123812.gif new file mode 100644 index 00000000..620156d1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/123812.gif differ diff --git a/Docs/04-UnrealEngine/Resources/123asd23422.gif b/Docs/04-UnrealEngine/Resources/123asd23422.gif new file mode 100644 index 00000000..ebf769fc Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/123asd23422.gif differ diff --git a/Docs/04-UnrealEngine/Resources/123d23422.gif b/Docs/04-UnrealEngine/Resources/123d23422.gif new file mode 100644 index 00000000..3b22970d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/123d23422.gif differ diff --git a/Docs/04-UnrealEngine/Resources/13.gif b/Docs/04-UnrealEngine/Resources/13.gif new file mode 100644 index 00000000..b78c379b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/13.gif differ diff --git a/Docs/04-UnrealEngine/Resources/148.gif b/Docs/04-UnrealEngine/Resources/148.gif new file mode 100644 index 00000000..02a22692 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/148.gif differ diff --git a/Docs/04-UnrealEngine/Resources/18.gif b/Docs/04-UnrealEngine/Resources/18.gif new file mode 100644 index 00000000..f3ff2904 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/18.gif differ diff --git a/Docs/04-UnrealEngine/Resources/53.gif b/Docs/04-UnrealEngine/Resources/53.gif new file mode 100644 index 00000000..eaa6e2f7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/53.gif differ diff --git a/Docs/04-UnrealEngine/Resources/9796875423a0a9928413d91f8c7420c2.png b/Docs/04-UnrealEngine/Resources/9796875423a0a9928413d91f8c7420c2.png new file mode 100644 index 00000000..29348293 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/9796875423a0a9928413d91f8c7420c2.png differ diff --git a/Docs/04-UnrealEngine/Resources/CameraCulling.gif b/Docs/04-UnrealEngine/Resources/CameraCulling.gif new file mode 100644 index 00000000..daca8591 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/CameraCulling.gif differ diff --git a/Docs/04-UnrealEngine/Resources/ClassDiagram-16558062433008.png b/Docs/04-UnrealEngine/Resources/ClassDiagram-16558062433008.png new file mode 100644 index 00000000..487f1766 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/ClassDiagram-16558062433008.png differ diff --git a/Docs/04-UnrealEngine/Resources/ClassDiagram.png b/Docs/04-UnrealEngine/Resources/ClassDiagram.png new file mode 100644 index 00000000..69ce986b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/ClassDiagram.png differ diff --git a/Docs/04-UnrealEngine/Resources/OIP-C.jpeg b/Docs/04-UnrealEngine/Resources/OIP-C.jpeg new file mode 100644 index 00000000..08ae1f0c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/OIP-C.jpeg differ diff --git a/Docs/04-UnrealEngine/Resources/The-structure-of-wav-file-format.png b/Docs/04-UnrealEngine/Resources/The-structure-of-wav-file-format.png new file mode 100644 index 00000000..e39bc185 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/The-structure-of-wav-file-format.png differ diff --git a/Docs/04-UnrealEngine/Resources/cpasd.gif b/Docs/04-UnrealEngine/Resources/cpasd.gif new file mode 100644 index 00000000..cb68c4d4 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/cpasd.gif differ diff --git a/Docs/04-UnrealEngine/Resources/cpsadasd.gif b/Docs/04-UnrealEngine/Resources/cpsadasd.gif new file mode 100644 index 00000000..d26a5900 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/cpsadasd.gif differ diff --git a/Docs/04-UnrealEngine/Resources/dasd.gif b/Docs/04-UnrealEngine/Resources/dasd.gif new file mode 100644 index 00000000..4745c0c9 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/dasd.gif differ diff --git a/Docs/04-UnrealEngine/Resources/dasssd.gif b/Docs/04-UnrealEngine/Resources/dasssd.gif new file mode 100644 index 00000000..d0190060 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/dasssd.gif differ diff --git a/Docs/04-UnrealEngine/Resources/dasssssd.gif b/Docs/04-UnrealEngine/Resources/dasssssd.gif new file mode 100644 index 00000000..57ae7841 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/dasssssd.gif differ diff --git a/Docs/04-UnrealEngine/Resources/dfgdasdd.gif b/Docs/04-UnrealEngine/Resources/dfgdasdd.gif new file mode 100644 index 00000000..f01e9f47 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/dfgdasdd.gif differ diff --git a/Docs/04-UnrealEngine/Resources/dfgdasdd1.gif b/Docs/04-UnrealEngine/Resources/dfgdasdd1.gif new file mode 100644 index 00000000..f6e61539 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/dfgdasdd1.gif differ diff --git a/Docs/04-UnrealEngine/Resources/format,png.png b/Docs/04-UnrealEngine/Resources/format,png.png new file mode 100644 index 00000000..bb2b207b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/format,png.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511094505941.png b/Docs/04-UnrealEngine/Resources/image-20220511094505941.png new file mode 100644 index 00000000..c44f51f4 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511094505941.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511094613291.png b/Docs/04-UnrealEngine/Resources/image-20220511094613291.png new file mode 100644 index 00000000..643a3038 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511094613291.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511095059287.png b/Docs/04-UnrealEngine/Resources/image-20220511095059287.png new file mode 100644 index 00000000..af467021 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511095059287.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511100541901.png b/Docs/04-UnrealEngine/Resources/image-20220511100541901.png new file mode 100644 index 00000000..bd12738e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511100541901.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511111624457.png b/Docs/04-UnrealEngine/Resources/image-20220511111624457.png new file mode 100644 index 00000000..ff0cb8b5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511111624457.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511134938844.png b/Docs/04-UnrealEngine/Resources/image-20220511134938844.png new file mode 100644 index 00000000..54423439 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511134938844.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511135106912.png b/Docs/04-UnrealEngine/Resources/image-20220511135106912.png new file mode 100644 index 00000000..71114dca Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511135106912.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511135145760.png b/Docs/04-UnrealEngine/Resources/image-20220511135145760.png new file mode 100644 index 00000000..b86cb622 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511135145760.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511135305680.png b/Docs/04-UnrealEngine/Resources/image-20220511135305680.png new file mode 100644 index 00000000..74cb9fca Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511135305680.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511193808966.png b/Docs/04-UnrealEngine/Resources/image-20220511193808966.png new file mode 100644 index 00000000..335d159b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511193808966.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220511193845152.png b/Docs/04-UnrealEngine/Resources/image-20220511193845152.png new file mode 100644 index 00000000..492edd50 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220511193845152.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220512115707800.png b/Docs/04-UnrealEngine/Resources/image-20220512115707800.png new file mode 100644 index 00000000..62e806b3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220512115707800.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220512164154229.png b/Docs/04-UnrealEngine/Resources/image-20220512164154229.png new file mode 100644 index 00000000..56e1996b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220512164154229.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220512164416438.png b/Docs/04-UnrealEngine/Resources/image-20220512164416438.png new file mode 100644 index 00000000..6d0659c2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220512164416438.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220512164449376.png b/Docs/04-UnrealEngine/Resources/image-20220512164449376.png new file mode 100644 index 00000000..be78ccbf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220512164449376.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220512165138234.png b/Docs/04-UnrealEngine/Resources/image-20220512165138234.png new file mode 100644 index 00000000..4660ba8d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220512165138234.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220513095022324.png b/Docs/04-UnrealEngine/Resources/image-20220513095022324.png new file mode 100644 index 00000000..82481d62 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220513095022324.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220513103652594.png b/Docs/04-UnrealEngine/Resources/image-20220513103652594.png new file mode 100644 index 00000000..cef59c28 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220513103652594.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220513104640466.png b/Docs/04-UnrealEngine/Resources/image-20220513104640466.png new file mode 100644 index 00000000..a96218b7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220513104640466.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220513104730072.png b/Docs/04-UnrealEngine/Resources/image-20220513104730072.png new file mode 100644 index 00000000..b3ddab5c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220513104730072.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518154617249.png b/Docs/04-UnrealEngine/Resources/image-20220518154617249.png new file mode 100644 index 00000000..39fef9f6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518154617249.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518154711351.png b/Docs/04-UnrealEngine/Resources/image-20220518154711351.png new file mode 100644 index 00000000..ae2753ca Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518154711351.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518154846902.png b/Docs/04-UnrealEngine/Resources/image-20220518154846902.png new file mode 100644 index 00000000..55749a08 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518154846902.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518155900834.png b/Docs/04-UnrealEngine/Resources/image-20220518155900834.png new file mode 100644 index 00000000..8aa8898d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518155900834.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518160850714.png b/Docs/04-UnrealEngine/Resources/image-20220518160850714.png new file mode 100644 index 00000000..48e56ac7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518160850714.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518161209322.png b/Docs/04-UnrealEngine/Resources/image-20220518161209322.png new file mode 100644 index 00000000..dd5356df Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518161209322.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518161727667.png b/Docs/04-UnrealEngine/Resources/image-20220518161727667.png new file mode 100644 index 00000000..4ec6e206 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518161727667.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518162532859.png b/Docs/04-UnrealEngine/Resources/image-20220518162532859.png new file mode 100644 index 00000000..ebbc1425 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518162532859.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518163837277.png b/Docs/04-UnrealEngine/Resources/image-20220518163837277.png new file mode 100644 index 00000000..93f3ea41 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518163837277.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518163912751.png b/Docs/04-UnrealEngine/Resources/image-20220518163912751.png new file mode 100644 index 00000000..ad9b191f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518163912751.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518163925567.png b/Docs/04-UnrealEngine/Resources/image-20220518163925567.png new file mode 100644 index 00000000..dca6ab7f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518163925567.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518164200510.png b/Docs/04-UnrealEngine/Resources/image-20220518164200510.png new file mode 100644 index 00000000..ace4daea Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518164200510.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518164213105.png b/Docs/04-UnrealEngine/Resources/image-20220518164213105.png new file mode 100644 index 00000000..4a5e8a3a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518164213105.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518165125687.png b/Docs/04-UnrealEngine/Resources/image-20220518165125687.png new file mode 100644 index 00000000..51500d0f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518165125687.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518171119875.png b/Docs/04-UnrealEngine/Resources/image-20220518171119875.png new file mode 100644 index 00000000..88deef9f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518171119875.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518171137321.png b/Docs/04-UnrealEngine/Resources/image-20220518171137321.png new file mode 100644 index 00000000..dc099147 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518171137321.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518171209154.png b/Docs/04-UnrealEngine/Resources/image-20220518171209154.png new file mode 100644 index 00000000..bca63c0c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518171209154.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518171848647.png b/Docs/04-UnrealEngine/Resources/image-20220518171848647.png new file mode 100644 index 00000000..b8c4c686 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518171848647.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518172336987.png b/Docs/04-UnrealEngine/Resources/image-20220518172336987.png new file mode 100644 index 00000000..e19ca62a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518172336987.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518175311526.png b/Docs/04-UnrealEngine/Resources/image-20220518175311526.png new file mode 100644 index 00000000..430c838c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518175311526.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518175922270.png b/Docs/04-UnrealEngine/Resources/image-20220518175922270.png new file mode 100644 index 00000000..8bd96401 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518175922270.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518180009013.png b/Docs/04-UnrealEngine/Resources/image-20220518180009013.png new file mode 100644 index 00000000..0c3152f4 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518180009013.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220518181105385.png b/Docs/04-UnrealEngine/Resources/image-20220518181105385.png new file mode 100644 index 00000000..82bd8b36 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220518181105385.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519100214978.png b/Docs/04-UnrealEngine/Resources/image-20220519100214978.png new file mode 100644 index 00000000..e8b5ad33 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519100214978.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519101516637.png b/Docs/04-UnrealEngine/Resources/image-20220519101516637.png new file mode 100644 index 00000000..f7cd07e0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519101516637.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519101724243.png b/Docs/04-UnrealEngine/Resources/image-20220519101724243.png new file mode 100644 index 00000000..af83acdf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519101724243.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519102706748.png b/Docs/04-UnrealEngine/Resources/image-20220519102706748.png new file mode 100644 index 00000000..ce98e72e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519102706748.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519102729140.png b/Docs/04-UnrealEngine/Resources/image-20220519102729140.png new file mode 100644 index 00000000..56b0477d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519102729140.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519102738687.png b/Docs/04-UnrealEngine/Resources/image-20220519102738687.png new file mode 100644 index 00000000..f2535136 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519102738687.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519104631179.png b/Docs/04-UnrealEngine/Resources/image-20220519104631179.png new file mode 100644 index 00000000..426dd8ff Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519104631179.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519105945628.png b/Docs/04-UnrealEngine/Resources/image-20220519105945628.png new file mode 100644 index 00000000..4014c470 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519105945628.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519110629347.png b/Docs/04-UnrealEngine/Resources/image-20220519110629347.png new file mode 100644 index 00000000..1defe009 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519110629347.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519113722921.png b/Docs/04-UnrealEngine/Resources/image-20220519113722921.png new file mode 100644 index 00000000..e1238f93 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519113722921.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519114840338.png b/Docs/04-UnrealEngine/Resources/image-20220519114840338.png new file mode 100644 index 00000000..c51c4596 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519114840338.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519133519815.png b/Docs/04-UnrealEngine/Resources/image-20220519133519815.png new file mode 100644 index 00000000..5817f0a6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519133519815.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519133531893.png b/Docs/04-UnrealEngine/Resources/image-20220519133531893.png new file mode 100644 index 00000000..31d99be8 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519133531893.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519133607430.png b/Docs/04-UnrealEngine/Resources/image-20220519133607430.png new file mode 100644 index 00000000..16657fae Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519133607430.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519133723353.png b/Docs/04-UnrealEngine/Resources/image-20220519133723353.png new file mode 100644 index 00000000..84c35f1b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519133723353.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519134544114.png b/Docs/04-UnrealEngine/Resources/image-20220519134544114.png new file mode 100644 index 00000000..5e301eb3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519134544114.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519141108149.png b/Docs/04-UnrealEngine/Resources/image-20220519141108149.png new file mode 100644 index 00000000..0dd3efbd Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519141108149.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519142042405.png b/Docs/04-UnrealEngine/Resources/image-20220519142042405.png new file mode 100644 index 00000000..a5d31646 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519142042405.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519142057130.png b/Docs/04-UnrealEngine/Resources/image-20220519142057130.png new file mode 100644 index 00000000..f43feca5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519142057130.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519143516261.png b/Docs/04-UnrealEngine/Resources/image-20220519143516261.png new file mode 100644 index 00000000..0e60ca5d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519143516261.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519144306639.png b/Docs/04-UnrealEngine/Resources/image-20220519144306639.png new file mode 100644 index 00000000..e7285a92 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519144306639.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519144844487.png b/Docs/04-UnrealEngine/Resources/image-20220519144844487.png new file mode 100644 index 00000000..3c348bcb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519144844487.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519151549183.png b/Docs/04-UnrealEngine/Resources/image-20220519151549183.png new file mode 100644 index 00000000..5c5f6789 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519151549183.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519151600924.png b/Docs/04-UnrealEngine/Resources/image-20220519151600924.png new file mode 100644 index 00000000..f993f58d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519151600924.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220519151610023.png b/Docs/04-UnrealEngine/Resources/image-20220519151610023.png new file mode 100644 index 00000000..24b23c85 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220519151610023.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220523194419184.png b/Docs/04-UnrealEngine/Resources/image-20220523194419184.png new file mode 100644 index 00000000..6514751f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220523194419184.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220523194840789.png b/Docs/04-UnrealEngine/Resources/image-20220523194840789.png new file mode 100644 index 00000000..44008bd0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220523194840789.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220523195615080.png b/Docs/04-UnrealEngine/Resources/image-20220523195615080.png new file mode 100644 index 00000000..7ddb0687 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220523195615080.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220523201230911.png b/Docs/04-UnrealEngine/Resources/image-20220523201230911.png new file mode 100644 index 00000000..377bda1d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220523201230911.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220523201257497.png b/Docs/04-UnrealEngine/Resources/image-20220523201257497.png new file mode 100644 index 00000000..a245bbeb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220523201257497.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220523202054386.png b/Docs/04-UnrealEngine/Resources/image-20220523202054386.png new file mode 100644 index 00000000..5b6df382 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220523202054386.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220524104514387.png b/Docs/04-UnrealEngine/Resources/image-20220524104514387.png new file mode 100644 index 00000000..98449b60 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220524104514387.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220525153230470.png b/Docs/04-UnrealEngine/Resources/image-20220525153230470.png new file mode 100644 index 00000000..0c7ec3d2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220525153230470.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220525180303122.png b/Docs/04-UnrealEngine/Resources/image-20220525180303122.png new file mode 100644 index 00000000..9ea17b1a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220525180303122.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526100116105.png b/Docs/04-UnrealEngine/Resources/image-20220526100116105.png new file mode 100644 index 00000000..ee505088 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526100116105.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526100207733.png b/Docs/04-UnrealEngine/Resources/image-20220526100207733.png new file mode 100644 index 00000000..7c0d7ee1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526100207733.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526100322912.png b/Docs/04-UnrealEngine/Resources/image-20220526100322912.png new file mode 100644 index 00000000..0605b802 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526100322912.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526100437881.png b/Docs/04-UnrealEngine/Resources/image-20220526100437881.png new file mode 100644 index 00000000..753c85bb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526100437881.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526100806461.png b/Docs/04-UnrealEngine/Resources/image-20220526100806461.png new file mode 100644 index 00000000..3b448c26 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526100806461.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526100906991.png b/Docs/04-UnrealEngine/Resources/image-20220526100906991.png new file mode 100644 index 00000000..188fd374 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526100906991.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526100943159.png b/Docs/04-UnrealEngine/Resources/image-20220526100943159.png new file mode 100644 index 00000000..6c92c82e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526100943159.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526101233486.png b/Docs/04-UnrealEngine/Resources/image-20220526101233486.png new file mode 100644 index 00000000..3679222b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526101233486.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220526115630692.png b/Docs/04-UnrealEngine/Resources/image-20220526115630692.png new file mode 100644 index 00000000..c12bc3a4 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220526115630692.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220527145227465.png b/Docs/04-UnrealEngine/Resources/image-20220527145227465.png new file mode 100644 index 00000000..94e598a7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220527145227465.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220530102114860.png b/Docs/04-UnrealEngine/Resources/image-20220530102114860.png new file mode 100644 index 00000000..55d22ceb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220530102114860.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220606160311229.png b/Docs/04-UnrealEngine/Resources/image-20220606160311229.png new file mode 100644 index 00000000..dfe38785 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220606160311229.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220606160405963.png b/Docs/04-UnrealEngine/Resources/image-20220606160405963.png new file mode 100644 index 00000000..cc010f62 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220606160405963.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220608112145739.png b/Docs/04-UnrealEngine/Resources/image-20220608112145739.png new file mode 100644 index 00000000..5dd16063 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220608112145739.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220608113509402.png b/Docs/04-UnrealEngine/Resources/image-20220608113509402.png new file mode 100644 index 00000000..65595687 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220608113509402.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220608113523358.png b/Docs/04-UnrealEngine/Resources/image-20220608113523358.png new file mode 100644 index 00000000..302d5ffa Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220608113523358.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220608143926116.png b/Docs/04-UnrealEngine/Resources/image-20220608143926116.png new file mode 100644 index 00000000..c3f7af82 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220608143926116.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220608143952346.png b/Docs/04-UnrealEngine/Resources/image-20220608143952346.png new file mode 100644 index 00000000..75f3e13b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220608143952346.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220622144759914.png b/Docs/04-UnrealEngine/Resources/image-20220622144759914.png new file mode 100644 index 00000000..65148534 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220622144759914.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220622144802360.png b/Docs/04-UnrealEngine/Resources/image-20220622144802360.png new file mode 100644 index 00000000..65148534 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220622144802360.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220622175331091.png b/Docs/04-UnrealEngine/Resources/image-20220622175331091.png new file mode 100644 index 00000000..7aa9dc80 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220622175331091.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220622180328342.png b/Docs/04-UnrealEngine/Resources/image-20220622180328342.png new file mode 100644 index 00000000..5c50a875 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220622180328342.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220623155029414.png b/Docs/04-UnrealEngine/Resources/image-20220623155029414.png new file mode 100644 index 00000000..4d32d09f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220623155029414.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704140749968.png b/Docs/04-UnrealEngine/Resources/image-20220704140749968.png new file mode 100644 index 00000000..3a3d1433 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704140749968.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704140919133.png b/Docs/04-UnrealEngine/Resources/image-20220704140919133.png new file mode 100644 index 00000000..b13de0ba Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704140919133.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704142114009.png b/Docs/04-UnrealEngine/Resources/image-20220704142114009.png new file mode 100644 index 00000000..781aaa9b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704142114009.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704143011624.png b/Docs/04-UnrealEngine/Resources/image-20220704143011624.png new file mode 100644 index 00000000..a2e55d65 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704143011624.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704143050017.png b/Docs/04-UnrealEngine/Resources/image-20220704143050017.png new file mode 100644 index 00000000..d4a52690 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704143050017.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704143106818.png b/Docs/04-UnrealEngine/Resources/image-20220704143106818.png new file mode 100644 index 00000000..63613065 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704143106818.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704180132630.png b/Docs/04-UnrealEngine/Resources/image-20220704180132630.png new file mode 100644 index 00000000..e7bc1e19 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704180132630.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704181940868.png b/Docs/04-UnrealEngine/Resources/image-20220704181940868.png new file mode 100644 index 00000000..d141a4ab Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704181940868.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704182345504.png b/Docs/04-UnrealEngine/Resources/image-20220704182345504.png new file mode 100644 index 00000000..83ae3b3e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704182345504.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220704183127690.png b/Docs/04-UnrealEngine/Resources/image-20220704183127690.png new file mode 100644 index 00000000..18f40ee7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220704183127690.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220705114115160.png b/Docs/04-UnrealEngine/Resources/image-20220705114115160.png new file mode 100644 index 00000000..71ff426a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220705114115160.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220705142208404.png b/Docs/04-UnrealEngine/Resources/image-20220705142208404.png new file mode 100644 index 00000000..2568676a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220705142208404.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220705142910309.png b/Docs/04-UnrealEngine/Resources/image-20220705142910309.png new file mode 100644 index 00000000..3fc8c537 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220705142910309.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220705143831980.png b/Docs/04-UnrealEngine/Resources/image-20220705143831980.png new file mode 100644 index 00000000..2abec1ba Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220705143831980.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220705144750986.png b/Docs/04-UnrealEngine/Resources/image-20220705144750986.png new file mode 100644 index 00000000..5ae31d77 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220705144750986.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220705145222454.png b/Docs/04-UnrealEngine/Resources/image-20220705145222454.png new file mode 100644 index 00000000..0a36899b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220705145222454.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220705145342871.png b/Docs/04-UnrealEngine/Resources/image-20220705145342871.png new file mode 100644 index 00000000..5fdd0c25 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220705145342871.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220705150245240.png b/Docs/04-UnrealEngine/Resources/image-20220705150245240.png new file mode 100644 index 00000000..cd528b18 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220705150245240.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220927141254086-16735895184905-168492056446620.png b/Docs/04-UnrealEngine/Resources/image-20220927141254086-16735895184905-168492056446620.png new file mode 100644 index 00000000..45fb9e41 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220927141254086-16735895184905-168492056446620.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220927141254086-16735895184905.png b/Docs/04-UnrealEngine/Resources/image-20220927141254086-16735895184905.png new file mode 100644 index 00000000..45fb9e41 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220927141254086-16735895184905.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220927175435715-16735895166583-168492054713617.png b/Docs/04-UnrealEngine/Resources/image-20220927175435715-16735895166583-168492054713617.png new file mode 100644 index 00000000..bd4ca7b2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220927175435715-16735895166583-168492054713617.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220927175435715-16735895166583.png b/Docs/04-UnrealEngine/Resources/image-20220927175435715-16735895166583.png new file mode 100644 index 00000000..bd4ca7b2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220927175435715-16735895166583.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220927210941350-16735895089921-168492058032923.png b/Docs/04-UnrealEngine/Resources/image-20220927210941350-16735895089921-168492058032923.png new file mode 100644 index 00000000..104e0115 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220927210941350-16735895089921-168492058032923.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220927210941350-16735895089921.png b/Docs/04-UnrealEngine/Resources/image-20220927210941350-16735895089921.png new file mode 100644 index 00000000..104e0115 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220927210941350-16735895089921.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220928100532108-167358956103511-168492058697726.png b/Docs/04-UnrealEngine/Resources/image-20220928100532108-167358956103511-168492058697726.png new file mode 100644 index 00000000..cc6a4e23 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220928100532108-167358956103511-168492058697726.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220928100532108-167358956103511.png b/Docs/04-UnrealEngine/Resources/image-20220928100532108-167358956103511.png new file mode 100644 index 00000000..cc6a4e23 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220928100532108-167358956103511.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220928175121797-16735895312067.png b/Docs/04-UnrealEngine/Resources/image-20220928175121797-16735895312067.png new file mode 100644 index 00000000..b4734679 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220928175121797-16735895312067.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20220928202723812-16735895507339.png b/Docs/04-UnrealEngine/Resources/image-20220928202723812-16735895507339.png new file mode 100644 index 00000000..2a8f545d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20220928202723812-16735895507339.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230517115452232-16848129576282.png b/Docs/04-UnrealEngine/Resources/image-20230517115452232-16848129576282.png new file mode 100644 index 00000000..ba94e38b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230517115452232-16848129576282.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230517115452232.png b/Docs/04-UnrealEngine/Resources/image-20230517115452232.png new file mode 100644 index 00000000..ba94e38b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230517115452232.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523115125388.png b/Docs/04-UnrealEngine/Resources/image-20230523115125388.png new file mode 100644 index 00000000..f755f998 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523115125388.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523115940276.png b/Docs/04-UnrealEngine/Resources/image-20230523115940276.png new file mode 100644 index 00000000..dc9c3507 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523115940276.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523140419952.png b/Docs/04-UnrealEngine/Resources/image-20230523140419952.png new file mode 100644 index 00000000..f4e96e92 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523140419952.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523140518199.png b/Docs/04-UnrealEngine/Resources/image-20230523140518199.png new file mode 100644 index 00000000..3bd3f322 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523140518199.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523140739431.png b/Docs/04-UnrealEngine/Resources/image-20230523140739431.png new file mode 100644 index 00000000..b572d0a4 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523140739431.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523170954045.png b/Docs/04-UnrealEngine/Resources/image-20230523170954045.png new file mode 100644 index 00000000..7452ba03 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523170954045.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523174457486.png b/Docs/04-UnrealEngine/Resources/image-20230523174457486.png new file mode 100644 index 00000000..f18e5849 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523174457486.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523200403812.png b/Docs/04-UnrealEngine/Resources/image-20230523200403812.png new file mode 100644 index 00000000..749c7d1a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523200403812.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230523203638600.png b/Docs/04-UnrealEngine/Resources/image-20230523203638600.png new file mode 100644 index 00000000..ee9f8f33 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230523203638600.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230524105904811.png b/Docs/04-UnrealEngine/Resources/image-20230524105904811.png new file mode 100644 index 00000000..e3684722 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230524105904811.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230524154628869.png b/Docs/04-UnrealEngine/Resources/image-20230524154628869.png new file mode 100644 index 00000000..fe02728a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230524154628869.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230524155859423.png b/Docs/04-UnrealEngine/Resources/image-20230524155859423.png new file mode 100644 index 00000000..addecddb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230524155859423.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230524160554125.png b/Docs/04-UnrealEngine/Resources/image-20230524160554125.png new file mode 100644 index 00000000..410ef0ad Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230524160554125.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230524163937497.png b/Docs/04-UnrealEngine/Resources/image-20230524163937497.png new file mode 100644 index 00000000..3c0ac4ff Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230524163937497.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230524164244564.png b/Docs/04-UnrealEngine/Resources/image-20230524164244564.png new file mode 100644 index 00000000..fbbc8cba Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230524164244564.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230525143413268.png b/Docs/04-UnrealEngine/Resources/image-20230525143413268.png new file mode 100644 index 00000000..44a88b33 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230525143413268.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230525145122582.png b/Docs/04-UnrealEngine/Resources/image-20230525145122582.png new file mode 100644 index 00000000..59a1edd4 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230525145122582.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230525145306629.png b/Docs/04-UnrealEngine/Resources/image-20230525145306629.png new file mode 100644 index 00000000..791af2f9 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230525145306629.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230525145357430.png b/Docs/04-UnrealEngine/Resources/image-20230525145357430.png new file mode 100644 index 00000000..fd67a505 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230525145357430.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230525162605340.png b/Docs/04-UnrealEngine/Resources/image-20230525162605340.png new file mode 100644 index 00000000..186ef2e1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230525162605340.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230525165851860.png b/Docs/04-UnrealEngine/Resources/image-20230525165851860.png new file mode 100644 index 00000000..cb80b7a5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230525165851860.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230525170207274.png b/Docs/04-UnrealEngine/Resources/image-20230525170207274.png new file mode 100644 index 00000000..b1cd662c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230525170207274.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230530153625676.png b/Docs/04-UnrealEngine/Resources/image-20230530153625676.png new file mode 100644 index 00000000..617b4ecc Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230530153625676.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230530161318939.png b/Docs/04-UnrealEngine/Resources/image-20230530161318939.png new file mode 100644 index 00000000..2d40817d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230530161318939.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230530161340764.png b/Docs/04-UnrealEngine/Resources/image-20230530161340764.png new file mode 100644 index 00000000..a10400fe Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230530161340764.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230530165544358.png b/Docs/04-UnrealEngine/Resources/image-20230530165544358.png new file mode 100644 index 00000000..82cfab7a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230530165544358.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230530181510368.png b/Docs/04-UnrealEngine/Resources/image-20230530181510368.png new file mode 100644 index 00000000..caad3aa3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230530181510368.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230530183325442.png b/Docs/04-UnrealEngine/Resources/image-20230530183325442.png new file mode 100644 index 00000000..23f6dc1b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230530183325442.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531094205665.png b/Docs/04-UnrealEngine/Resources/image-20230531094205665.png new file mode 100644 index 00000000..9d0f2bf3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531094205665.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531094416657.png b/Docs/04-UnrealEngine/Resources/image-20230531094416657.png new file mode 100644 index 00000000..5aa35ffd Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531094416657.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531094851667.png b/Docs/04-UnrealEngine/Resources/image-20230531094851667.png new file mode 100644 index 00000000..acd22ee7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531094851667.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531134824751.png b/Docs/04-UnrealEngine/Resources/image-20230531134824751.png new file mode 100644 index 00000000..8ad8d834 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531134824751.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531164053994.png b/Docs/04-UnrealEngine/Resources/image-20230531164053994.png new file mode 100644 index 00000000..6be3ea04 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531164053994.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531165625590.png b/Docs/04-UnrealEngine/Resources/image-20230531165625590.png new file mode 100644 index 00000000..6c593945 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531165625590.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531165631521.png b/Docs/04-UnrealEngine/Resources/image-20230531165631521.png new file mode 100644 index 00000000..6c593945 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531165631521.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531180012391.png b/Docs/04-UnrealEngine/Resources/image-20230531180012391.png new file mode 100644 index 00000000..d5d4e1fb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531180012391.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230531182616000.png b/Docs/04-UnrealEngine/Resources/image-20230531182616000.png new file mode 100644 index 00000000..7f4660ec Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230531182616000.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230601182230193.png b/Docs/04-UnrealEngine/Resources/image-20230601182230193.png new file mode 100644 index 00000000..bd576131 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230601182230193.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230602103618309.png b/Docs/04-UnrealEngine/Resources/image-20230602103618309.png new file mode 100644 index 00000000..825971c6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230602103618309.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230803141535141.png b/Docs/04-UnrealEngine/Resources/image-20230803141535141.png new file mode 100644 index 00000000..6fd90569 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230803141535141.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20230803153605584-1699321940233-4.png b/Docs/04-UnrealEngine/Resources/image-20230803153605584-1699321940233-4.png new file mode 100644 index 00000000..d536db96 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20230803153605584-1699321940233-4.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007172724563.png b/Docs/04-UnrealEngine/Resources/image-20231007172724563.png new file mode 100644 index 00000000..a02fc688 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007172724563.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007175720103.png b/Docs/04-UnrealEngine/Resources/image-20231007175720103.png new file mode 100644 index 00000000..1dbf6058 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007175720103.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007180002716.png b/Docs/04-UnrealEngine/Resources/image-20231007180002716.png new file mode 100644 index 00000000..4346dc4d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007180002716.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007181531935.png b/Docs/04-UnrealEngine/Resources/image-20231007181531935.png new file mode 100644 index 00000000..0bcddc75 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007181531935.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007193640734.png b/Docs/04-UnrealEngine/Resources/image-20231007193640734.png new file mode 100644 index 00000000..a7208c44 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007193640734.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007193928904.png b/Docs/04-UnrealEngine/Resources/image-20231007193928904.png new file mode 100644 index 00000000..3f11a964 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007193928904.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007194409696.png b/Docs/04-UnrealEngine/Resources/image-20231007194409696.png new file mode 100644 index 00000000..ef48ac93 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007194409696.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007194552208.png b/Docs/04-UnrealEngine/Resources/image-20231007194552208.png new file mode 100644 index 00000000..2547cb1f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007194552208.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007194731440.png b/Docs/04-UnrealEngine/Resources/image-20231007194731440.png new file mode 100644 index 00000000..ea523176 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007194731440.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007195927854.png b/Docs/04-UnrealEngine/Resources/image-20231007195927854.png new file mode 100644 index 00000000..95080dbb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007195927854.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007200945698.png b/Docs/04-UnrealEngine/Resources/image-20231007200945698.png new file mode 100644 index 00000000..ab52999e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007200945698.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007201137432.png b/Docs/04-UnrealEngine/Resources/image-20231007201137432.png new file mode 100644 index 00000000..1f43f718 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007201137432.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007203244212.png b/Docs/04-UnrealEngine/Resources/image-20231007203244212.png new file mode 100644 index 00000000..241589e6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007203244212.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007203838084.png b/Docs/04-UnrealEngine/Resources/image-20231007203838084.png new file mode 100644 index 00000000..306cec46 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007203838084.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007204141184.png b/Docs/04-UnrealEngine/Resources/image-20231007204141184.png new file mode 100644 index 00000000..5f7b3475 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007204141184.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007204536387.png b/Docs/04-UnrealEngine/Resources/image-20231007204536387.png new file mode 100644 index 00000000..16f70171 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007204536387.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007204851243.png b/Docs/04-UnrealEngine/Resources/image-20231007204851243.png new file mode 100644 index 00000000..de7c2407 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007204851243.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007205104536.png b/Docs/04-UnrealEngine/Resources/image-20231007205104536.png new file mode 100644 index 00000000..3efbf107 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007205104536.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007205814852.png b/Docs/04-UnrealEngine/Resources/image-20231007205814852.png new file mode 100644 index 00000000..5aae2bb3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007205814852.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007210218516.png b/Docs/04-UnrealEngine/Resources/image-20231007210218516.png new file mode 100644 index 00000000..f3f3590f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007210218516.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007210228693.png b/Docs/04-UnrealEngine/Resources/image-20231007210228693.png new file mode 100644 index 00000000..aaa2096a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007210228693.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007210313044.png b/Docs/04-UnrealEngine/Resources/image-20231007210313044.png new file mode 100644 index 00000000..bd6af896 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007210313044.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007210419151.png b/Docs/04-UnrealEngine/Resources/image-20231007210419151.png new file mode 100644 index 00000000..0cf68b94 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007210419151.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007210636923.png b/Docs/04-UnrealEngine/Resources/image-20231007210636923.png new file mode 100644 index 00000000..01bd98dd Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007210636923.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007211058328.png b/Docs/04-UnrealEngine/Resources/image-20231007211058328.png new file mode 100644 index 00000000..f74010b2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007211058328.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007211256346.png b/Docs/04-UnrealEngine/Resources/image-20231007211256346.png new file mode 100644 index 00000000..22d636a7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007211256346.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231007211513307.png b/Docs/04-UnrealEngine/Resources/image-20231007211513307.png new file mode 100644 index 00000000..2be30b71 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231007211513307.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008101627932.png b/Docs/04-UnrealEngine/Resources/image-20231008101627932.png new file mode 100644 index 00000000..ab61d7d1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008101627932.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008101743881.png b/Docs/04-UnrealEngine/Resources/image-20231008101743881.png new file mode 100644 index 00000000..9a4b0f81 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008101743881.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008104748978.png b/Docs/04-UnrealEngine/Resources/image-20231008104748978.png new file mode 100644 index 00000000..17912c8d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008104748978.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008105515353.png b/Docs/04-UnrealEngine/Resources/image-20231008105515353.png new file mode 100644 index 00000000..fc24321c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008105515353.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008110203395.png b/Docs/04-UnrealEngine/Resources/image-20231008110203395.png new file mode 100644 index 00000000..05da6f6a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008110203395.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008110433919.png b/Docs/04-UnrealEngine/Resources/image-20231008110433919.png new file mode 100644 index 00000000..4346da7b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008110433919.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008110706037.png b/Docs/04-UnrealEngine/Resources/image-20231008110706037.png new file mode 100644 index 00000000..e39f846c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008110706037.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008111218076.png b/Docs/04-UnrealEngine/Resources/image-20231008111218076.png new file mode 100644 index 00000000..d1264ab8 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008111218076.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008113659412.png b/Docs/04-UnrealEngine/Resources/image-20231008113659412.png new file mode 100644 index 00000000..dd090563 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008113659412.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008114722968.png b/Docs/04-UnrealEngine/Resources/image-20231008114722968.png new file mode 100644 index 00000000..df433171 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008114722968.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008115241436.png b/Docs/04-UnrealEngine/Resources/image-20231008115241436.png new file mode 100644 index 00000000..d8bf0ce0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008115241436.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008115550644.png b/Docs/04-UnrealEngine/Resources/image-20231008115550644.png new file mode 100644 index 00000000..9ed24557 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008115550644.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008115733236.png b/Docs/04-UnrealEngine/Resources/image-20231008115733236.png new file mode 100644 index 00000000..48523244 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008115733236.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008145630953.png b/Docs/04-UnrealEngine/Resources/image-20231008145630953.png new file mode 100644 index 00000000..ba09d6a2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008145630953.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008145838367.png b/Docs/04-UnrealEngine/Resources/image-20231008145838367.png new file mode 100644 index 00000000..99f5790d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008145838367.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008150524786.png b/Docs/04-UnrealEngine/Resources/image-20231008150524786.png new file mode 100644 index 00000000..9c413530 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008150524786.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008151631152.png b/Docs/04-UnrealEngine/Resources/image-20231008151631152.png new file mode 100644 index 00000000..bf98825d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008151631152.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008151748551.png b/Docs/04-UnrealEngine/Resources/image-20231008151748551.png new file mode 100644 index 00000000..df3f2753 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008151748551.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231008160522818.png b/Docs/04-UnrealEngine/Resources/image-20231008160522818.png new file mode 100644 index 00000000..ba09d6a2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231008160522818.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231017111331540.png b/Docs/04-UnrealEngine/Resources/image-20231017111331540.png new file mode 100644 index 00000000..fa90c0c9 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231017111331540.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231017111622364.png b/Docs/04-UnrealEngine/Resources/image-20231017111622364.png new file mode 100644 index 00000000..83e16065 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231017111622364.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231017112528040.png b/Docs/04-UnrealEngine/Resources/image-20231017112528040.png new file mode 100644 index 00000000..dde9f3c0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231017112528040.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231017113022901.png b/Docs/04-UnrealEngine/Resources/image-20231017113022901.png new file mode 100644 index 00000000..62a4e453 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231017113022901.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231017150541788.png b/Docs/04-UnrealEngine/Resources/image-20231017150541788.png new file mode 100644 index 00000000..a039f68c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231017150541788.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231017171757122.png b/Docs/04-UnrealEngine/Resources/image-20231017171757122.png new file mode 100644 index 00000000..c8a60012 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231017171757122.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231017171821284.png b/Docs/04-UnrealEngine/Resources/image-20231017171821284.png new file mode 100644 index 00000000..f4d457f0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231017171821284.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231020113158786.png b/Docs/04-UnrealEngine/Resources/image-20231020113158786.png new file mode 100644 index 00000000..ce675fe3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231020113158786.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231020115448454.png b/Docs/04-UnrealEngine/Resources/image-20231020115448454.png new file mode 100644 index 00000000..a97b3d7e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231020115448454.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231020115915843.png b/Docs/04-UnrealEngine/Resources/image-20231020115915843.png new file mode 100644 index 00000000..f22c8120 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231020115915843.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231020115943300.png b/Docs/04-UnrealEngine/Resources/image-20231020115943300.png new file mode 100644 index 00000000..d9fa5840 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231020115943300.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231020133605969-1698391657801-4.png b/Docs/04-UnrealEngine/Resources/image-20231020133605969-1698391657801-4.png new file mode 100644 index 00000000..422a7625 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231020133605969-1698391657801-4.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231027153421929.png b/Docs/04-UnrealEngine/Resources/image-20231027153421929.png new file mode 100644 index 00000000..c288e28c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231027153421929.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231027153551427.png b/Docs/04-UnrealEngine/Resources/image-20231027153551427.png new file mode 100644 index 00000000..ca8a80a0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231027153551427.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231027153936040.png b/Docs/04-UnrealEngine/Resources/image-20231027153936040.png new file mode 100644 index 00000000..eb45c864 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231027153936040.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231027154047197.png b/Docs/04-UnrealEngine/Resources/image-20231027154047197.png new file mode 100644 index 00000000..9608e859 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231027154047197.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231027154204423.png b/Docs/04-UnrealEngine/Resources/image-20231027154204423.png new file mode 100644 index 00000000..d30336c9 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231027154204423.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231027163751104.png b/Docs/04-UnrealEngine/Resources/image-20231027163751104.png new file mode 100644 index 00000000..ee62b1dc Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231027163751104.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231027164043816.png b/Docs/04-UnrealEngine/Resources/image-20231027164043816.png new file mode 100644 index 00000000..7da15867 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231027164043816.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231103155440236.png b/Docs/04-UnrealEngine/Resources/image-20231103155440236.png new file mode 100644 index 00000000..ec06c322 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231103155440236.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231103161359654.png b/Docs/04-UnrealEngine/Resources/image-20231103161359654.png new file mode 100644 index 00000000..93ca2ecb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231103161359654.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231103164631165.png b/Docs/04-UnrealEngine/Resources/image-20231103164631165.png new file mode 100644 index 00000000..1fc18734 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231103164631165.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114145044372.png b/Docs/04-UnrealEngine/Resources/image-20231114145044372.png new file mode 100644 index 00000000..39ac4a7a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114145044372.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114145107565.png b/Docs/04-UnrealEngine/Resources/image-20231114145107565.png new file mode 100644 index 00000000..38fac39d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114145107565.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114145128735.png b/Docs/04-UnrealEngine/Resources/image-20231114145128735.png new file mode 100644 index 00000000..38fac39d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114145128735.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114145134503.png b/Docs/04-UnrealEngine/Resources/image-20231114145134503.png new file mode 100644 index 00000000..38fac39d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114145134503.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114145533712.png b/Docs/04-UnrealEngine/Resources/image-20231114145533712.png new file mode 100644 index 00000000..3ea06672 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114145533712.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114145942912.png b/Docs/04-UnrealEngine/Resources/image-20231114145942912.png new file mode 100644 index 00000000..333f6d37 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114145942912.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114152013980.png b/Docs/04-UnrealEngine/Resources/image-20231114152013980.png new file mode 100644 index 00000000..118f9030 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114152013980.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114160604396.png b/Docs/04-UnrealEngine/Resources/image-20231114160604396.png new file mode 100644 index 00000000..705ba56c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114160604396.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114161417804.png b/Docs/04-UnrealEngine/Resources/image-20231114161417804.png new file mode 100644 index 00000000..47cb8619 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114161417804.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114161611114.png b/Docs/04-UnrealEngine/Resources/image-20231114161611114.png new file mode 100644 index 00000000..4d9ded18 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114161611114.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114162146791.png b/Docs/04-UnrealEngine/Resources/image-20231114162146791.png new file mode 100644 index 00000000..79b322c5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114162146791.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114164608493.png b/Docs/04-UnrealEngine/Resources/image-20231114164608493.png new file mode 100644 index 00000000..666e5681 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114164608493.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114165051217.png b/Docs/04-UnrealEngine/Resources/image-20231114165051217.png new file mode 100644 index 00000000..3802239b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114165051217.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114165515082.png b/Docs/04-UnrealEngine/Resources/image-20231114165515082.png new file mode 100644 index 00000000..e6f43335 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114165515082.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114165552571.png b/Docs/04-UnrealEngine/Resources/image-20231114165552571.png new file mode 100644 index 00000000..2783b410 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114165552571.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114165855058.png b/Docs/04-UnrealEngine/Resources/image-20231114165855058.png new file mode 100644 index 00000000..441169a6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114165855058.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114195314683.png b/Docs/04-UnrealEngine/Resources/image-20231114195314683.png new file mode 100644 index 00000000..af07fcfb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114195314683.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114201238339.png b/Docs/04-UnrealEngine/Resources/image-20231114201238339.png new file mode 100644 index 00000000..30b9b896 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114201238339.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114202724330.png b/Docs/04-UnrealEngine/Resources/image-20231114202724330.png new file mode 100644 index 00000000..07796da1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114202724330.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231114202905246.png b/Docs/04-UnrealEngine/Resources/image-20231114202905246.png new file mode 100644 index 00000000..362ae676 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231114202905246.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115095919654.png b/Docs/04-UnrealEngine/Resources/image-20231115095919654.png new file mode 100644 index 00000000..8ab96289 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115095919654.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115100308055.png b/Docs/04-UnrealEngine/Resources/image-20231115100308055.png new file mode 100644 index 00000000..04dfd216 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115100308055.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115101605904.png b/Docs/04-UnrealEngine/Resources/image-20231115101605904.png new file mode 100644 index 00000000..5ee9ee1b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115101605904.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115101956749.png b/Docs/04-UnrealEngine/Resources/image-20231115101956749.png new file mode 100644 index 00000000..8d563805 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115101956749.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115102115854.png b/Docs/04-UnrealEngine/Resources/image-20231115102115854.png new file mode 100644 index 00000000..be07feaf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115102115854.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115102315733.png b/Docs/04-UnrealEngine/Resources/image-20231115102315733.png new file mode 100644 index 00000000..e0426323 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115102315733.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115103426806.png b/Docs/04-UnrealEngine/Resources/image-20231115103426806.png new file mode 100644 index 00000000..f3060580 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115103426806.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115103750131.png b/Docs/04-UnrealEngine/Resources/image-20231115103750131.png new file mode 100644 index 00000000..0debb6de Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115103750131.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115104949193.png b/Docs/04-UnrealEngine/Resources/image-20231115104949193.png new file mode 100644 index 00000000..1c94d4b6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115104949193.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115104955903.png b/Docs/04-UnrealEngine/Resources/image-20231115104955903.png new file mode 100644 index 00000000..1c94d4b6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115104955903.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115105156054.png b/Docs/04-UnrealEngine/Resources/image-20231115105156054.png new file mode 100644 index 00000000..2d2d1433 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115105156054.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115105732103.png b/Docs/04-UnrealEngine/Resources/image-20231115105732103.png new file mode 100644 index 00000000..2d2d1433 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115105732103.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115105833878.png b/Docs/04-UnrealEngine/Resources/image-20231115105833878.png new file mode 100644 index 00000000..9af0de9e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115105833878.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115110648420.png b/Docs/04-UnrealEngine/Resources/image-20231115110648420.png new file mode 100644 index 00000000..16918070 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115110648420.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115110654951.png b/Docs/04-UnrealEngine/Resources/image-20231115110654951.png new file mode 100644 index 00000000..16918070 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115110654951.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115111345866.png b/Docs/04-UnrealEngine/Resources/image-20231115111345866.png new file mode 100644 index 00000000..24cdda16 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115111345866.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115111735046.png b/Docs/04-UnrealEngine/Resources/image-20231115111735046.png new file mode 100644 index 00000000..f446186c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115111735046.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115112113233.png b/Docs/04-UnrealEngine/Resources/image-20231115112113233.png new file mode 100644 index 00000000..8f9eaea5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115112113233.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115112418360.png b/Docs/04-UnrealEngine/Resources/image-20231115112418360.png new file mode 100644 index 00000000..586ba48d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115112418360.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115112955341.png b/Docs/04-UnrealEngine/Resources/image-20231115112955341.png new file mode 100644 index 00000000..b9bd94d0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115112955341.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115113119585.png b/Docs/04-UnrealEngine/Resources/image-20231115113119585.png new file mode 100644 index 00000000..ebdd02cf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115113119585.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115113520770.png b/Docs/04-UnrealEngine/Resources/image-20231115113520770.png new file mode 100644 index 00000000..47e48884 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115113520770.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115114230488.png b/Docs/04-UnrealEngine/Resources/image-20231115114230488.png new file mode 100644 index 00000000..47e48884 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115114230488.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115181025276.png b/Docs/04-UnrealEngine/Resources/image-20231115181025276.png new file mode 100644 index 00000000..d1e2ee50 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115181025276.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231115182237273.png b/Docs/04-UnrealEngine/Resources/image-20231115182237273.png new file mode 100644 index 00000000..2ca3f8c2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231115182237273.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117161054031.png b/Docs/04-UnrealEngine/Resources/image-20231117161054031.png new file mode 100644 index 00000000..8a26794f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117161054031.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117161142891.png b/Docs/04-UnrealEngine/Resources/image-20231117161142891.png new file mode 100644 index 00000000..f61ed86f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117161142891.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117161325291.png b/Docs/04-UnrealEngine/Resources/image-20231117161325291.png new file mode 100644 index 00000000..20dd2fbb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117161325291.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117161508777.png b/Docs/04-UnrealEngine/Resources/image-20231117161508777.png new file mode 100644 index 00000000..7385b680 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117161508777.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117161748514.png b/Docs/04-UnrealEngine/Resources/image-20231117161748514.png new file mode 100644 index 00000000..ed75e850 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117161748514.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117162047588.png b/Docs/04-UnrealEngine/Resources/image-20231117162047588.png new file mode 100644 index 00000000..8cb40e51 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117162047588.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117162207129.png b/Docs/04-UnrealEngine/Resources/image-20231117162207129.png new file mode 100644 index 00000000..7e5ac7c8 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117162207129.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117162611951.png b/Docs/04-UnrealEngine/Resources/image-20231117162611951.png new file mode 100644 index 00000000..7697bdbb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117162611951.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117163147270.png b/Docs/04-UnrealEngine/Resources/image-20231117163147270.png new file mode 100644 index 00000000..c071e9a6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117163147270.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231117165456545.png b/Docs/04-UnrealEngine/Resources/image-20231117165456545.png new file mode 100644 index 00000000..9e2f096c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231117165456545.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231124103614783.png b/Docs/04-UnrealEngine/Resources/image-20231124103614783.png new file mode 100644 index 00000000..f14a7c29 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231124103614783.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231127182325974.png b/Docs/04-UnrealEngine/Resources/image-20231127182325974.png new file mode 100644 index 00000000..0b76869b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231127182325974.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231127182512039.png b/Docs/04-UnrealEngine/Resources/image-20231127182512039.png new file mode 100644 index 00000000..58b0d52e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231127182512039.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128100134840.png b/Docs/04-UnrealEngine/Resources/image-20231128100134840.png new file mode 100644 index 00000000..49ada693 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128100134840.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128100717690.png b/Docs/04-UnrealEngine/Resources/image-20231128100717690.png new file mode 100644 index 00000000..55aa999b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128100717690.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128101425779.png b/Docs/04-UnrealEngine/Resources/image-20231128101425779.png new file mode 100644 index 00000000..6faa6431 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128101425779.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128102516857.png b/Docs/04-UnrealEngine/Resources/image-20231128102516857.png new file mode 100644 index 00000000..8ba5dea2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128102516857.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128103524123.png b/Docs/04-UnrealEngine/Resources/image-20231128103524123.png new file mode 100644 index 00000000..18906fed Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128103524123.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128110811971.png b/Docs/04-UnrealEngine/Resources/image-20231128110811971.png new file mode 100644 index 00000000..f9ff4608 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128110811971.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128111209378.png b/Docs/04-UnrealEngine/Resources/image-20231128111209378.png new file mode 100644 index 00000000..40411f19 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128111209378.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128111308024.png b/Docs/04-UnrealEngine/Resources/image-20231128111308024.png new file mode 100644 index 00000000..4e6c53d9 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128111308024.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128111425188.png b/Docs/04-UnrealEngine/Resources/image-20231128111425188.png new file mode 100644 index 00000000..1fc18734 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128111425188.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128111620135.png b/Docs/04-UnrealEngine/Resources/image-20231128111620135.png new file mode 100644 index 00000000..55d8920f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128111620135.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128115206771.png b/Docs/04-UnrealEngine/Resources/image-20231128115206771.png new file mode 100644 index 00000000..55aa999b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128115206771.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128134921374.png b/Docs/04-UnrealEngine/Resources/image-20231128134921374.png new file mode 100644 index 00000000..6325ce5e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128134921374.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128135313851.png b/Docs/04-UnrealEngine/Resources/image-20231128135313851.png new file mode 100644 index 00000000..745a0304 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128135313851.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128183041540.png b/Docs/04-UnrealEngine/Resources/image-20231128183041540.png new file mode 100644 index 00000000..e69de29b diff --git a/Docs/04-UnrealEngine/Resources/image-20231128183045032.png b/Docs/04-UnrealEngine/Resources/image-20231128183045032.png new file mode 100644 index 00000000..e69de29b diff --git a/Docs/04-UnrealEngine/Resources/image-20231128183116324.png b/Docs/04-UnrealEngine/Resources/image-20231128183116324.png new file mode 100644 index 00000000..7963d49f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128183116324.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128183145993.png b/Docs/04-UnrealEngine/Resources/image-20231128183145993.png new file mode 100644 index 00000000..49ada693 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128183145993.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231128203302065.png b/Docs/04-UnrealEngine/Resources/image-20231128203302065.png new file mode 100644 index 00000000..6faa6431 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231128203302065.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129094519448.png b/Docs/04-UnrealEngine/Resources/image-20231129094519448.png new file mode 100644 index 00000000..55aa999b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129094519448.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129112140082.png b/Docs/04-UnrealEngine/Resources/image-20231129112140082.png new file mode 100644 index 00000000..4394a14c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129112140082.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129112158262.png b/Docs/04-UnrealEngine/Resources/image-20231129112158262.png new file mode 100644 index 00000000..4394a14c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129112158262.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129134856514.png b/Docs/04-UnrealEngine/Resources/image-20231129134856514.png new file mode 100644 index 00000000..193d307c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129134856514.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129135711872.png b/Docs/04-UnrealEngine/Resources/image-20231129135711872.png new file mode 100644 index 00000000..fc2e3b07 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129135711872.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129150658814.png b/Docs/04-UnrealEngine/Resources/image-20231129150658814.png new file mode 100644 index 00000000..ef39afe4 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129150658814.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129151231027.png b/Docs/04-UnrealEngine/Resources/image-20231129151231027.png new file mode 100644 index 00000000..f79733c5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129151231027.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129151341077.png b/Docs/04-UnrealEngine/Resources/image-20231129151341077.png new file mode 100644 index 00000000..6faa6431 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129151341077.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129152538095.png b/Docs/04-UnrealEngine/Resources/image-20231129152538095.png new file mode 100644 index 00000000..f73b539a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129152538095.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129153201753.png b/Docs/04-UnrealEngine/Resources/image-20231129153201753.png new file mode 100644 index 00000000..2e96d058 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129153201753.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129153317971.png b/Docs/04-UnrealEngine/Resources/image-20231129153317971.png new file mode 100644 index 00000000..f73b539a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129153317971.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129153324440.png b/Docs/04-UnrealEngine/Resources/image-20231129153324440.png new file mode 100644 index 00000000..2e96d058 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129153324440.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129153331203.png b/Docs/04-UnrealEngine/Resources/image-20231129153331203.png new file mode 100644 index 00000000..2e96d058 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129153331203.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129161229035.png b/Docs/04-UnrealEngine/Resources/image-20231129161229035.png new file mode 100644 index 00000000..7e85dd15 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129161229035.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129161335413.png b/Docs/04-UnrealEngine/Resources/image-20231129161335413.png new file mode 100644 index 00000000..54d490a3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129161335413.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231129161940907.png b/Docs/04-UnrealEngine/Resources/image-20231129161940907.png new file mode 100644 index 00000000..020026af Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231129161940907.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231130100005205.png b/Docs/04-UnrealEngine/Resources/image-20231130100005205.png new file mode 100644 index 00000000..715cf21f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231130100005205.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231130101753095.png b/Docs/04-UnrealEngine/Resources/image-20231130101753095.png new file mode 100644 index 00000000..9470a863 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231130101753095.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231201112501518.png b/Docs/04-UnrealEngine/Resources/image-20231201112501518.png new file mode 100644 index 00000000..2908f115 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231201112501518.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231204160944855.png b/Docs/04-UnrealEngine/Resources/image-20231204160944855.png new file mode 100644 index 00000000..48d33c78 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231204160944855.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231211095416739.png b/Docs/04-UnrealEngine/Resources/image-20231211095416739.png new file mode 100644 index 00000000..223bdeb9 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231211095416739.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231211100646090.png b/Docs/04-UnrealEngine/Resources/image-20231211100646090.png new file mode 100644 index 00000000..e4f915df Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231211100646090.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219115229767.png b/Docs/04-UnrealEngine/Resources/image-20231219115229767.png new file mode 100644 index 00000000..52c2813c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219115229767.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219140643992.png b/Docs/04-UnrealEngine/Resources/image-20231219140643992.png new file mode 100644 index 00000000..f1d3833a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219140643992.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219142512174.png b/Docs/04-UnrealEngine/Resources/image-20231219142512174.png new file mode 100644 index 00000000..33fe5637 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219142512174.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219150044226.png b/Docs/04-UnrealEngine/Resources/image-20231219150044226.png new file mode 100644 index 00000000..ba04c3e6 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219150044226.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219150125135.png b/Docs/04-UnrealEngine/Resources/image-20231219150125135.png new file mode 100644 index 00000000..b6e07360 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219150125135.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219150159319.png b/Docs/04-UnrealEngine/Resources/image-20231219150159319.png new file mode 100644 index 00000000..5a4ec1e5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219150159319.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219150330364.png b/Docs/04-UnrealEngine/Resources/image-20231219150330364.png new file mode 100644 index 00000000..495cf5a0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219150330364.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219152126701.png b/Docs/04-UnrealEngine/Resources/image-20231219152126701.png new file mode 100644 index 00000000..22b3b5ba Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219152126701.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219154945838.png b/Docs/04-UnrealEngine/Resources/image-20231219154945838.png new file mode 100644 index 00000000..e7db21ea Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219154945838.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219155025102.png b/Docs/04-UnrealEngine/Resources/image-20231219155025102.png new file mode 100644 index 00000000..c9c78479 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219155025102.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219155252825.png b/Docs/04-UnrealEngine/Resources/image-20231219155252825.png new file mode 100644 index 00000000..272debcc Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219155252825.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219171750987.png b/Docs/04-UnrealEngine/Resources/image-20231219171750987.png new file mode 100644 index 00000000..82a6e95a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219171750987.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20231219172035691.png b/Docs/04-UnrealEngine/Resources/image-20231219172035691.png new file mode 100644 index 00000000..5f2f8a6e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20231219172035691.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204102619133.png b/Docs/04-UnrealEngine/Resources/image-20240204102619133.png new file mode 100644 index 00000000..76ab8008 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204102619133.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204103616957.png b/Docs/04-UnrealEngine/Resources/image-20240204103616957.png new file mode 100644 index 00000000..d09c8e9e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204103616957.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204104205505.png b/Docs/04-UnrealEngine/Resources/image-20240204104205505.png new file mode 100644 index 00000000..4a6f3a6c Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204104205505.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204105850144.png b/Docs/04-UnrealEngine/Resources/image-20240204105850144.png new file mode 100644 index 00000000..9ac19343 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204105850144.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204142115830.png b/Docs/04-UnrealEngine/Resources/image-20240204142115830.png new file mode 100644 index 00000000..45c3341f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204142115830.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204155736479.png b/Docs/04-UnrealEngine/Resources/image-20240204155736479.png new file mode 100644 index 00000000..540a212d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204155736479.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204170415081.png b/Docs/04-UnrealEngine/Resources/image-20240204170415081.png new file mode 100644 index 00000000..e84f0b4b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204170415081.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204191436270.png b/Docs/04-UnrealEngine/Resources/image-20240204191436270.png new file mode 100644 index 00000000..d9a07e49 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204191436270.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204191454472.png b/Docs/04-UnrealEngine/Resources/image-20240204191454472.png new file mode 100644 index 00000000..a52ee427 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204191454472.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204195219544.png b/Docs/04-UnrealEngine/Resources/image-20240204195219544.png new file mode 100644 index 00000000..cf89d25f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204195219544.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204195409513.png b/Docs/04-UnrealEngine/Resources/image-20240204195409513.png new file mode 100644 index 00000000..763ef070 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204195409513.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240204204639645.png b/Docs/04-UnrealEngine/Resources/image-20240204204639645.png new file mode 100644 index 00000000..804794fc Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240204204639645.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205104528872.png b/Docs/04-UnrealEngine/Resources/image-20240205104528872.png new file mode 100644 index 00000000..8174876a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205104528872.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205111218033.png b/Docs/04-UnrealEngine/Resources/image-20240205111218033.png new file mode 100644 index 00000000..6d58c738 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205111218033.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205111652587.png b/Docs/04-UnrealEngine/Resources/image-20240205111652587.png new file mode 100644 index 00000000..ac37b546 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205111652587.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205142028396.png b/Docs/04-UnrealEngine/Resources/image-20240205142028396.png new file mode 100644 index 00000000..05f97ed1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205142028396.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205142303554.png b/Docs/04-UnrealEngine/Resources/image-20240205142303554.png new file mode 100644 index 00000000..82e19eea Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205142303554.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205143150293.png b/Docs/04-UnrealEngine/Resources/image-20240205143150293.png new file mode 100644 index 00000000..fe47a499 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205143150293.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205143955462.png b/Docs/04-UnrealEngine/Resources/image-20240205143955462.png new file mode 100644 index 00000000..09bc540d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205143955462.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205144327362.png b/Docs/04-UnrealEngine/Resources/image-20240205144327362.png new file mode 100644 index 00000000..eb7ee981 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205144327362.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205150518808.png b/Docs/04-UnrealEngine/Resources/image-20240205150518808.png new file mode 100644 index 00000000..60770525 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205150518808.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205150739219.png b/Docs/04-UnrealEngine/Resources/image-20240205150739219.png new file mode 100644 index 00000000..6e8ffaea Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205150739219.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205152403078.png b/Docs/04-UnrealEngine/Resources/image-20240205152403078.png new file mode 100644 index 00000000..a2d8bfee Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205152403078.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205152805417.png b/Docs/04-UnrealEngine/Resources/image-20240205152805417.png new file mode 100644 index 00000000..4514db6d Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205152805417.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205154502428.png b/Docs/04-UnrealEngine/Resources/image-20240205154502428.png new file mode 100644 index 00000000..45c1b996 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205154502428.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205155420006.png b/Docs/04-UnrealEngine/Resources/image-20240205155420006.png new file mode 100644 index 00000000..b3fbfae1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205155420006.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205155953096.png b/Docs/04-UnrealEngine/Resources/image-20240205155953096.png new file mode 100644 index 00000000..6667348b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205155953096.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205160115633.png b/Docs/04-UnrealEngine/Resources/image-20240205160115633.png new file mode 100644 index 00000000..9f0573bf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205160115633.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205160406177.png b/Docs/04-UnrealEngine/Resources/image-20240205160406177.png new file mode 100644 index 00000000..4ed5d4e1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205160406177.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205161259486.png b/Docs/04-UnrealEngine/Resources/image-20240205161259486.png new file mode 100644 index 00000000..bf5019cf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205161259486.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205162426317.png b/Docs/04-UnrealEngine/Resources/image-20240205162426317.png new file mode 100644 index 00000000..87202e4f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205162426317.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205162859891.png b/Docs/04-UnrealEngine/Resources/image-20240205162859891.png new file mode 100644 index 00000000..076ea8d3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205162859891.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205162920488.png b/Docs/04-UnrealEngine/Resources/image-20240205162920488.png new file mode 100644 index 00000000..2f190442 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205162920488.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240205181636978.png b/Docs/04-UnrealEngine/Resources/image-20240205181636978.png new file mode 100644 index 00000000..1a0c1bb0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240205181636978.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240622180634547.png b/Docs/04-UnrealEngine/Resources/image-20240622180634547.png new file mode 100644 index 00000000..cbf2f992 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240622180634547.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240622180652772.png b/Docs/04-UnrealEngine/Resources/image-20240622180652772.png new file mode 100644 index 00000000..10cde75e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240622180652772.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240622181134887.png b/Docs/04-UnrealEngine/Resources/image-20240622181134887.png new file mode 100644 index 00000000..7b8f8cfb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240622181134887.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623091909278.png b/Docs/04-UnrealEngine/Resources/image-20240623091909278.png new file mode 100644 index 00000000..1abcf4b2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623091909278.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623091927982.png b/Docs/04-UnrealEngine/Resources/image-20240623091927982.png new file mode 100644 index 00000000..3e5771b2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623091927982.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623092017252.png b/Docs/04-UnrealEngine/Resources/image-20240623092017252.png new file mode 100644 index 00000000..4c7e496b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623092017252.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623092039005.png b/Docs/04-UnrealEngine/Resources/image-20240623092039005.png new file mode 100644 index 00000000..fe52a3d0 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623092039005.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623092124043.png b/Docs/04-UnrealEngine/Resources/image-20240623092124043.png new file mode 100644 index 00000000..f9dbd55f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623092124043.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623092259966.png b/Docs/04-UnrealEngine/Resources/image-20240623092259966.png new file mode 100644 index 00000000..0315aecf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623092259966.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623092410845.png b/Docs/04-UnrealEngine/Resources/image-20240623092410845.png new file mode 100644 index 00000000..8ba63955 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623092410845.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623094904708.png b/Docs/04-UnrealEngine/Resources/image-20240623094904708.png new file mode 100644 index 00000000..425af78b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623094904708.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623095438717.png b/Docs/04-UnrealEngine/Resources/image-20240623095438717.png new file mode 100644 index 00000000..57b0a510 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623095438717.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623100042218.png b/Docs/04-UnrealEngine/Resources/image-20240623100042218.png new file mode 100644 index 00000000..077b69d9 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623100042218.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623100114909.png b/Docs/04-UnrealEngine/Resources/image-20240623100114909.png new file mode 100644 index 00000000..375d74bf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623100114909.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623100423208.png b/Docs/04-UnrealEngine/Resources/image-20240623100423208.png new file mode 100644 index 00000000..33bceb01 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623100423208.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623102606582.png b/Docs/04-UnrealEngine/Resources/image-20240623102606582.png new file mode 100644 index 00000000..9d1603ba Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623102606582.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240623102635905.png b/Docs/04-UnrealEngine/Resources/image-20240623102635905.png new file mode 100644 index 00000000..0473142e Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240623102635905.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808105838771.png b/Docs/04-UnrealEngine/Resources/image-20240808105838771.png new file mode 100644 index 00000000..c1b5b955 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808105838771.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808105845345.png b/Docs/04-UnrealEngine/Resources/image-20240808105845345.png new file mode 100644 index 00000000..ec1fbdaf Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808105845345.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808134017142.png b/Docs/04-UnrealEngine/Resources/image-20240808134017142.png new file mode 100644 index 00000000..d4140243 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808134017142.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808134539736.png b/Docs/04-UnrealEngine/Resources/image-20240808134539736.png new file mode 100644 index 00000000..5ffbb850 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808134539736.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808134944231.png b/Docs/04-UnrealEngine/Resources/image-20240808134944231.png new file mode 100644 index 00000000..be54a91b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808134944231.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808135113053.png b/Docs/04-UnrealEngine/Resources/image-20240808135113053.png new file mode 100644 index 00000000..00072386 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808135113053.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808135612492.png b/Docs/04-UnrealEngine/Resources/image-20240808135612492.png new file mode 100644 index 00000000..7378f2c2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808135612492.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808141433678.png b/Docs/04-UnrealEngine/Resources/image-20240808141433678.png new file mode 100644 index 00000000..612ef87a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808141433678.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808182351274.png b/Docs/04-UnrealEngine/Resources/image-20240808182351274.png new file mode 100644 index 00000000..66555545 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808182351274.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808193318835.png b/Docs/04-UnrealEngine/Resources/image-20240808193318835.png new file mode 100644 index 00000000..3bdf2b11 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808193318835.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808195330576.png b/Docs/04-UnrealEngine/Resources/image-20240808195330576.png new file mode 100644 index 00000000..7a71ca0b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808195330576.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808202239795.png b/Docs/04-UnrealEngine/Resources/image-20240808202239795.png new file mode 100644 index 00000000..a7708866 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808202239795.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808203040303.png b/Docs/04-UnrealEngine/Resources/image-20240808203040303.png new file mode 100644 index 00000000..f3c35bcc Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808203040303.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240808203242532.png b/Docs/04-UnrealEngine/Resources/image-20240808203242532.png new file mode 100644 index 00000000..bfb416a7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240808203242532.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809095412837.png b/Docs/04-UnrealEngine/Resources/image-20240809095412837.png new file mode 100644 index 00000000..e09c6248 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809095412837.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809135411348.png b/Docs/04-UnrealEngine/Resources/image-20240809135411348.png new file mode 100644 index 00000000..6a286312 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809135411348.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809151946858.png b/Docs/04-UnrealEngine/Resources/image-20240809151946858.png new file mode 100644 index 00000000..3e5c4f5b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809151946858.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809161248634.png b/Docs/04-UnrealEngine/Resources/image-20240809161248634.png new file mode 100644 index 00000000..98f10ae9 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809161248634.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809161621579.png b/Docs/04-UnrealEngine/Resources/image-20240809161621579.png new file mode 100644 index 00000000..67cd7a1a Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809161621579.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809170000187.png b/Docs/04-UnrealEngine/Resources/image-20240809170000187.png new file mode 100644 index 00000000..93ca2ecb Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809170000187.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809171548496.png b/Docs/04-UnrealEngine/Resources/image-20240809171548496.png new file mode 100644 index 00000000..f70625d7 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809171548496.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809171835592.png b/Docs/04-UnrealEngine/Resources/image-20240809171835592.png new file mode 100644 index 00000000..5d60d4e1 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809171835592.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809173840266.png b/Docs/04-UnrealEngine/Resources/image-20240809173840266.png new file mode 100644 index 00000000..8bea6e64 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809173840266.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809174827247.png b/Docs/04-UnrealEngine/Resources/image-20240809174827247.png new file mode 100644 index 00000000..5ca13c2f Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809174827247.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809175349308.png b/Docs/04-UnrealEngine/Resources/image-20240809175349308.png new file mode 100644 index 00000000..5ab94d72 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809175349308.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809175526774.png b/Docs/04-UnrealEngine/Resources/image-20240809175526774.png new file mode 100644 index 00000000..ae3b7581 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809175526774.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809175559308.png b/Docs/04-UnrealEngine/Resources/image-20240809175559308.png new file mode 100644 index 00000000..9cf36b80 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809175559308.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809193856172.png b/Docs/04-UnrealEngine/Resources/image-20240809193856172.png new file mode 100644 index 00000000..b610f822 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809193856172.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809195258560.png b/Docs/04-UnrealEngine/Resources/image-20240809195258560.png new file mode 100644 index 00000000..23beaa5b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809195258560.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809195933115.png b/Docs/04-UnrealEngine/Resources/image-20240809195933115.png new file mode 100644 index 00000000..7e2faa16 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809195933115.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809195940083.png b/Docs/04-UnrealEngine/Resources/image-20240809195940083.png new file mode 100644 index 00000000..7e2faa16 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809195940083.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809201526488.png b/Docs/04-UnrealEngine/Resources/image-20240809201526488.png new file mode 100644 index 00000000..2dbd95d3 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809201526488.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809201801468.png b/Docs/04-UnrealEngine/Resources/image-20240809201801468.png new file mode 100644 index 00000000..849129da Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809201801468.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809203942983.png b/Docs/04-UnrealEngine/Resources/image-20240809203942983.png new file mode 100644 index 00000000..d8d16304 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809203942983.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809223452013.png b/Docs/04-UnrealEngine/Resources/image-20240809223452013.png new file mode 100644 index 00000000..6696a3a8 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809223452013.png differ diff --git a/Docs/04-UnrealEngine/Resources/image-20240809224217321.png b/Docs/04-UnrealEngine/Resources/image-20240809224217321.png new file mode 100644 index 00000000..8f55e3bc Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/image-20240809224217321.png differ diff --git a/Docs/04-UnrealEngine/Resources/sdsssd.gif b/Docs/04-UnrealEngine/Resources/sdsssd.gif new file mode 100644 index 00000000..efcc6cf2 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/sdsssd.gif differ diff --git a/Docs/04-UnrealEngine/Resources/sfsa.gif b/Docs/04-UnrealEngine/Resources/sfsa.gif new file mode 100644 index 00000000..24b6311b Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/sfsa.gif differ diff --git a/Docs/04-UnrealEngine/Resources/v2-ac82f9df3eb6d0ab62edc448957c80e6_r.jpg b/Docs/04-UnrealEngine/Resources/v2-ac82f9df3eb6d0ab62edc448957c80e6_r.jpg new file mode 100644 index 00000000..422ba2a5 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/v2-ac82f9df3eb6d0ab62edc448957c80e6_r.jpg differ diff --git a/Docs/04-UnrealEngine/Resources/viewing-frustum-_frustum.fit_lim.size_768x.gif b/Docs/04-UnrealEngine/Resources/viewing-frustum-_frustum.fit_lim.size_768x.gif new file mode 100644 index 00000000..bdb7fe23 Binary files /dev/null and b/Docs/04-UnrealEngine/Resources/viewing-frustum-_frustum.fit_lim.size_768x.gif differ diff --git "a/Docs/04-UnrealEngine/Resources/\345\233\276\347\211\2072.png" "b/Docs/04-UnrealEngine/Resources/\345\233\276\347\211\2072.png" new file mode 100644 index 00000000..96f6e3cd Binary files /dev/null and "b/Docs/04-UnrealEngine/Resources/\345\233\276\347\211\2072.png" differ diff --git "a/Docs/04-UnrealEngine/Resources/\345\233\276\347\211\2073.png" "b/Docs/04-UnrealEngine/Resources/\345\233\276\347\211\2073.png" new file mode 100644 index 00000000..63c23a2d Binary files /dev/null and "b/Docs/04-UnrealEngine/Resources/\345\233\276\347\211\2073.png" differ diff --git "a/Docs/04-UnrealEngine/Resources/\345\276\256\344\277\241\345\233\276\347\211\207_20240808105819.png" "b/Docs/04-UnrealEngine/Resources/\345\276\256\344\277\241\345\233\276\347\211\207_20240808105819.png" new file mode 100644 index 00000000..c909e678 Binary files /dev/null and "b/Docs/04-UnrealEngine/Resources/\345\276\256\344\277\241\345\233\276\347\211\207_20240808105819.png" differ diff --git "a/Docs/05-Other/0.\345\274\200\346\224\276\344\270\226\347\225\214\346\235\202\350\260\210.md" "b/Docs/05-Other/0.\345\274\200\346\224\276\344\270\226\347\225\214\346\235\202\350\260\210.md" new file mode 100644 index 00000000..10ff5890 --- /dev/null +++ "b/Docs/05-Other/0.\345\274\200\346\224\276\344\270\226\347\225\214\346\235\202\350\260\210.md" @@ -0,0 +1,349 @@ +# 游戏开发杂谈-开放世界 + +> **开放世界(Open World)** 是一个[玩家](https://en.wikipedia.org/wiki/Gamer)可以在自由地实现目标的[虚拟世界](https://en.wikipedia.org/wiki/Virtual_world),而不是具有更线性和结构化[游戏玩法的](https://en.wikipedia.org/wiki/Gameplay)世界。其中的著名游戏包括[《塞尔达传说》](https://en.wikipedia.org/wiki/The_Legend_of_Zelda_(video_game))、[《侠盗猎车手》](https://en.wikipedia.org/wiki/Grand_Theft_Auto_III)、 [《荒野大镖客》](https://zh.wikipedia.org/wiki/%E7%A2%A7%E8%A1%80%E7%8B%82%E6%AE%BA%E7%B3%BB%E5%88%97) 、[《上古卷轴》](https://zh.wikipedia.org/wiki/%E4%B8%8A%E5%8F%A4%E5%8D%B7%E8%BD%B4%E7%B3%BB%E5%88%97)、[《我的世界》](https://en.wikipedia.org/wiki/Minecraft),[《艾尔登法环》](https://zh.wikipedia.org/wiki/%E8%89%BE%E7%88%BE%E7%99%BB%E6%B3%95%E7%92%B0)... +> +> 开放世界的重点在于开放性,因此开放世界游戏一般在在内存上会动态且无缝的加载游戏世界,这类游戏的主要吸引力在于为玩家提供自主权,使得游戏进程可以按玩家预期的顺序和方式来推进,但不是在游戏中可以做他们想做的任何事情(这在当下的计算机技术几乎是不可能的)。 +> +> ——摘自 [《Wiki - Openg World》](https://en.wikipedia.org/wiki/Open_world) + +关于开放世界的关卡设计,这里有一些非常优秀的文档: + +- [制作一个开放世界的游戏有哪些挑战?- 天美工作室 / 江岸栖][https://www.zhihu.com/question/336988349/answer/2174068590] + +- [从零开始,做一款开放世界 - 游戏葡萄 / wenlon](https://mp.weixin.qq.com/s?__biz=MjM5OTc2ODUxMw==&mid=2649897301&idx=1&sn=87e94966851b1d3a3e7abad542be6689) + +> 笔者是一名入行不久的引擎工程师,本文主要用于记录和讨论,将以程序开发的视角去剖析开放世界,一些想法和思考方式可能过于片面,欢迎大家指正。 + +## 开放世界面临的挑战 + +在知乎上有一个相关的讨论: + +- [知乎 - 制作一个开放世界的游戏有哪些挑战?](https://www.zhihu.com/question/336988349) + +有[回答](https://www.zhihu.com/question/336988349/answer/2821336649)提到,开放世界的主要挑战是: + +- 开发成本 +- 游戏引擎 +- 策划剧情 +- 原画风格 +- 硬件资源 +- 游戏玩法 +- 项目管理 +- 版本管理 +- 宣传推广 +- 发行渠道 + +笔者是程序开发人员,对`原画风格`,`策划剧情`,`宣传推广`,`发行渠道` 这些问题了解不多,接触过一些相关人员,他们对这些问题有着优秀的解决方案,或许目前而言,这并不是太大的问题。 + +### 开发成本 + +这里的开发成本具体指的是 **资金成本** 和 **时间成本** + +这里有一个关于资金成本的列表: + +> 完整表单来自于:https://en.wikipedia.org/wiki/List_of_most_expensive_video_games_to_develop + +| 名称 | 年份 | 开发商 | 出版商 | 平台 | 开发成本 (百万美元) | 营销成本 (百万美元) | 总成本 (百万美元) | 2022 年通货膨胀总成本 (百万美元) | +| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :-------------------: | :-------------------: | :-----------------: | :--------------------------------: | +| *[星际公民](https://en.wikipedia.org/wiki/Star_Citizen)* | [待定](https://en.wikipedia.org/wiki/List_of_most_expensive_video_games_to_develop#cite_note-4) | Cloud Imperium Games | Cloud Imperium Games | Windows | 415+ | 86+ | 501+ | 541+ | +| *[侠盗车手IV](https://en.wikipedia.org/wiki/Grand_Theft_Auto_IV)* | 2008 | [Rockstar North](https://en.wikipedia.org/wiki/Rockstar_North) | [Rockstar Games](https://en.wikipedia.org/wiki/Rockstar_Games) | [PS3](https://en.wikipedia.org/wiki/PS3)、[Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | | | 100+ | 136+ | +| *[侠盗猎车手V](https://en.wikipedia.org/wiki/Grand_Theft_Auto_V)* | 2013 | [Rockstar Studios](https://en.wikipedia.org/wiki/Rockstar_Studios) | [Rockstar Games](https://en.wikipedia.org/wiki/Rockstar_Games) | [PS4](https://en.wikipedia.org/wiki/PS4), [Xbox One](https://en.wikipedia.org/wiki/Xbox_One) | | | 265 | 333 | +| *[荒野大镖客:救赎](https://en.wikipedia.org/wiki/Red_Dead_Redemption)* | 2010 | [Rockstar San Diego](https://en.wikipedia.org/wiki/Rockstar_San_Diego) | [Rockstar Games](https://en.wikipedia.org/wiki/Rockstar_Games) | [PS3](https://en.wikipedia.org/wiki/PS3), [Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | | | 80-100 | 107-134 | +| *[荒野大镖客:救赎 2](https://en.wikipedia.org/wiki/Red_Dead_Redemption_2)* | 2018 | [Rockstar Studios](https://en.wikipedia.org/wiki/Rockstar_Studios) | [Rockstar Games](https://en.wikipedia.org/wiki/Rockstar_Games) | [PS4](https://en.wikipedia.org/wiki/PS4), [Xbox One](https://en.wikipedia.org/wiki/Xbox_One) | | | 200-300 | 233-350 | +| *[赛博朋克2077](https://en.wikipedia.org/wiki/Cyberpunk_2077)* | 2020 | [CD Projekt Red](https://en.wikipedia.org/wiki/CD_Projekt_Red) | [ CD Projekt](https://en.wikipedia.org/wiki/CD_Projekt) | [PS4](https://en.wikipedia.org/wiki/PS4), [Stadia](https://en.wikipedia.org/wiki/Google_Stadia), [Windows](https://en.wikipedia.org/wiki/Windows), [Xbox One](https://en.wikipedia.org/wiki/Xbox_One) | 174 | 142 | 316 | 357 | +| *[使命召唤:现代战争 2](https://en.wikipedia.org/wiki/Call_of_Duty:_Modern_Warfare_2)* | 2009 | [Infinity Ward](https://en.wikipedia.org/wiki/Infinity_Ward) | [动视](https://en.wikipedia.org/wiki/Activision) | [ PS3](https://en.wikipedia.org/wiki/PS3), [Windows](https://en.wikipedia.org/wiki/Windows), [Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | 40–50 | 200 | 240–250 | 327–341 | +| *[最终幻想VII](https://en.wikipedia.org/wiki/Final_Fantasy_VII)* | 1997 | [Square](https://en.wikipedia.org/wiki/Square_(video_game_company)) | [Square](https://en.wikipedia.org/wiki/Square_(video_game_company)), [Sony Computer Entertainment](https://en.wikipedia.org/wiki/Sony_Computer_Entertainment) | [PS1](https://en.wikipedia.org/wiki/PlayStation_(console)) | 40–45 | 40–100 | 80–145 | 146–264 | +| *[最后生还者 2](https://en.wikipedia.org/wiki/The_Last_of_Us_Part_II)* | 2020 | [顽皮狗](https://en.wikipedia.org/wiki/Naughty_Dog) | [索尼互动娱乐](https://en.wikipedia.org/wiki/Sony_Interactive_Entertainment) | [PS4](https://en.wikipedia.org/wiki/PS4) | 220 | | 220 | 249 | +| *[光环2](https://en.wikipedia.org/wiki/Halo_2)* | 2004 | [ Bungie](https://en.wikipedia.org/wiki/Bungie) | [微软工作室](https://en.wikipedia.org/wiki/Microsoft_Studios) | [Xbox](https://en.wikipedia.org/wiki/Xbox_(console)) | 40 | 80 | 120 | 248 | +| *[地平线:西之绝境](https://en.wikipedia.org/wiki/Horizon_Forbidden_West)* | 2022 | [Guerrilla Games](https://en.wikipedia.org/wiki/Guerrilla_Games) | [索尼互动娱乐](https://en.wikipedia.org/wiki/Sony_Interactive_Entertainment) | [PS4](https://en.wikipedia.org/wiki/PS4) , [PS5](https://en.wikipedia.org/wiki/PS5) | 212 | | 212 | 212 | +| *[命运](https://en.wikipedia.org/wiki/Destiny_(video_game))* | 2014 | [ Bungie](https://en.wikipedia.org/wiki/Bungie) | [动视](https://en.wikipedia.org/wiki/Activision) | [PS3](https://en.wikipedia.org/wiki/PS3)、[PS4](https://en.wikipedia.org/wiki/PS4)、[Xbox 360](https://en.wikipedia.org/wiki/Xbox_360)、[Xbox One](https://en.wikipedia.org/wiki/Xbox_One) | | | 140 | 173 | +| *[古墓丽影:暗影](https://en.wikipedia.org/wiki/Shadow_of_the_Tomb_Raider)* | 2018 | [Eidos Montréal](https://en.wikipedia.org/wiki/Eidos_Montréal) | [Square Enix](https://en.wikipedia.org/wiki/Square_Enix) | [PS4](https://en.wikipedia.org/wiki/PlayStation_4) , [Windows](https://en.wikipedia.org/wiki/Windows) , [Xbox One](https://en.wikipedia.org/wiki/Xbox_One) | 75–100 | 35 | 110–135 | 128–157 | +| *[死亡空间2](https://en.wikipedia.org/wiki/Dead_Space_2)* | 2011 | [Visceral Games](https://en.wikipedia.org/wiki/Visceral_Games) | [Electronic Arts](https://en.wikipedia.org/wiki/Electronic_Arts) | [PS3](https://en.wikipedia.org/wiki/PS3) , [Windows](https://en.wikipedia.org/wiki/Windows) , [Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | 60 | 60 | 120 | 156 | +| *[莎木](https://en.wikipedia.org/wiki/Shenmue_(video_game))* | 1999 | [世嘉AM2](https://en.wikipedia.org/wiki/Sega_AM2) | [世嘉](https://en.wikipedia.org/wiki/Sega) | [ Dreamcast](https://en.wikipedia.org/wiki/Dreamcast) | 47 | | 47–70 | 83–123 | +| *[战地4](https://en.wikipedia.org/wiki/Battlefield_4)* | 2013 | [ EA DICE](https://en.wikipedia.org/wiki/EA_DICE) | [Electronic_Arts](https://en.wikipedia.org/wiki/Electronic_Arts) | [PS3](https://en.wikipedia.org/wiki/PS3) , [Windows](https://en.wikipedia.org/wiki/Windows) , [Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | 100 | | 100 | 126 | +| *[原神](https://en.wikipedia.org/wiki/Genshin_Impact)* | 2020 | [米哈游](https://en.wikipedia.org/wiki/MiHoYo) | [米哈游](https://en.wikipedia.org/wiki/MiHoYo) | [安卓](https://en.wikipedia.org/wiki/Android_(operating_system))、[iOS](https://en.wikipedia.org/wiki/IOS)、[PS4](https://en.wikipedia.org/wiki/PS4)、[Windows](https://en.wikipedia.org/wiki/Windows) | | | 100+ | 113+ | +| *[巫师 3:狂猎](https://en.wikipedia.org/wiki/The_Witcher_3:_Wild_Hunt)* | 2015 | [ CD Projekt Red](https://en.wikipedia.org/wiki/CD_Projekt_Red) | [CD Projekt](https://en.wikipedia.org/wiki/CD_Projekt) | [PS4](https://en.wikipedia.org/wiki/PS4) , [Windows](https://en.wikipedia.org/wiki/Windows) , [Xbox One](https://en.wikipedia.org/wiki/Xbox_One) | | | 81 | 100 | +| *[裂痕](https://en.wikipedia.org/wiki/Rift_(video_game))* | 2011 | [Trion Worlds](https://en.wikipedia.org/wiki/Trion_Worlds) | [Trion Worlds](https://en.wikipedia.org/wiki/Trion_Worlds) | [Windows](https://en.wikipedia.org/wiki/Windows) | 50+ | 10–20 | 60–70+ | 78–91+ | +| *[看门狗](https://en.wikipedia.org/wiki/Watch_Dogs_(video_game))* | 2014 | [Ubisoft Montreal](https://en.wikipedia.org/wiki/Ubisoft_Montreal) | [育碧](https://en.wikipedia.org/wiki/Ubisoft) | [PS3](https://en.wikipedia.org/wiki/PS3)、[PS4](https://en.wikipedia.org/wiki/PS4)、[Windows](https://en.wikipedia.org/wiki/Windows)、[Xbox 360](https://en.wikipedia.org/wiki/Xbox_360)、[Xbox One](https://en.wikipedia.org/wiki/Xbox_One) | 68+ | | 68+ | 84+ | +| *[幽灵行动4:未来战士](https://en.wikipedia.org/wiki/Tom_Clancy's_Ghost_Recon:_Future_Soldier)* | 2012 | [Ubisoft Paris](https://en.wikipedia.org/wiki/Ubisoft_Paris), [Red Storm Entertainment](https://en.wikipedia.org/wiki/Red_Storm_Entertainment), [Ubisoft Bucharest](https://en.wikipedia.org/wiki/Ubisoft_Bucharest) | [育碧](https://en.wikipedia.org/wiki/Ubisoft) | [PS3](https://en.wikipedia.org/wiki/PS3)、[Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | | | 65 | 83 | +| *[GT赛车5](https://en.wikipedia.org/wiki/Gran_Turismo_5)* | 2010 | [Polyphony Digital](https://en.wikipedia.org/wiki/Polyphony_Digital) | [索尼电脑娱乐](https://en.wikipedia.org/wiki/Sony_Computer_Entertainment) | [PS3](https://en.wikipedia.org/wiki/PS3) | 60 | | 60 | 81 | +| *[战争机器3](https://en.wikipedia.org/wiki/Gears_of_War_3)* | 2011 | [Epic Games](https://en.wikipedia.org/wiki/Epic_Games) | [微软工作室](https://en.wikipedia.org/wiki/Microsoft_Studios) | [Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | 48–60 | | 48–60 | 62–78 | +| *[战争机器:审判](https://en.wikipedia.org/wiki/Gears_of_War:_Judgment)* | 2013 | [Epic Games](https://en.wikipedia.org/wiki/Epic_Games) | [微软工作室](https://en.wikipedia.org/wiki/Microsoft_Studios) | [Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | | | 60 | 75 | +| *[倾盆大雨](https://en.wikipedia.org/wiki/Heavy_Rain)* | 2010 | [Quantic Dream](https://en.wikipedia.org/wiki/Quantic_Dream) | [索尼电脑娱乐](https://en.wikipedia.org/wiki/Sony_Computer_Entertainment) | [PS3](https://en.wikipedia.org/wiki/PS3) | 21.8 | 30.4 | 52.2 | 70 | +| *[最终幻想9](https://en.wikipedia.org/wiki/Final_Fantasy_IX)* | 2000 | [Square](https://en.wikipedia.org/wiki/Square_(video_game_company)) | [Square](https://en.wikipedia.org/wiki/Square_(video_game_company)) | [PS1](https://en.wikipedia.org/wiki/PlayStation_(console)) | 40 | | 40+ | 68+ | +| *[ET 外星人](https://en.wikipedia.org/wiki/E.T._the_Extra-Terrestrial_(video_game))* | 1982 | [Atari, Inc.](https://en.wikipedia.org/wiki/Atari,_Inc.) | [Atari, Inc.](https://en.wikipedia.org/wiki/Atari,_Inc.) | [Atari 2600](https://en.wikipedia.org/wiki/Atari_2600) | | | 22 | 67 | +| *[暗黑血统 II](https://en.wikipedia.org/wiki/Darksiders_II)* | 2012 | [ Vigil Games](https://en.wikipedia.org/wiki/Vigil_Games) | [THQ](https://en.wikipedia.org/wiki/THQ) | [PS3](https://en.wikipedia.org/wiki/PS3) , [Windows](https://en.wikipedia.org/wiki/Windows) , [Xbox 360](https://en.wikipedia.org/wiki/Xbox_360) | | | 50 | 64 | +| *[半条命2](https://en.wikipedia.org/wiki/Half-Life_2)* | 2004 | [Valve](https://en.wikipedia.org/wiki/Valve_Corporation) | [Valve](https://en.wikipedia.org/wiki/Valve_Corporation) | [Windows](https://en.wikipedia.org/wiki/Windows) | 40 | | 40 | 62 | +| *[战火王国 II](https://en.wikipedia.org/wiki/Kingdom_Under_Fire_II)* | 2019 | [Blueside](https://en.wikipedia.org/wiki/Blueside), [Phantagram](https://en.wikipedia.org/wiki/Phantagram) | [Gameforge](https://en.wikipedia.org/wiki/Gameforge) | [Windows](https://en.wikipedia.org/wiki/Windows) | 50+ | | | 62 | + +从上述表单可以了解到,一些开放世界大作动辄就是千万甚至上亿美元,不过也存在像 [我的世界(Minecraft)](https://en.wikipedia.org/wiki/Minecraft)这样的异类。 + +关于具体的资金划分,可以参考: + +- [[Bilibili] 获得游戏奥斯卡需要多少预算?只狼开发成本和职能比例盘点【行业幕后02】- 鱼一元00](https://www.bilibili.com/video/BV14J411k7eH) + +> 开放世界大作与之类似,相比之下有着更大的体量,其不同之处,可能是还需要增加额外的场景管理人员 + +可以做一个简单的估算: + +- 100人团队,每人平均20W/年,那么一年下来单纯员工薪资的开销就近3000w,再加上设备,福利,外包等一系列杂七杂八的费用,一年下来可能就差不多四五千万,按大部分大型游戏的开发周期为5年左右,再加上后期的营销和运营费用,也就是说,如果想开发一款大型的游戏,可能至少需要两亿人民币的预算。 + +而这还是在理想情况下,实际上我们还面临着更大的难题: + +- 国内游戏行业起步较晚,很多方面的积累都不尽人意,打个比方,《塞尔达传说:王国之泪》,《艾尔登法环》的开发时间大约是4年,那么国内团队在资金充足的条件下,也能用四年制作出同样体量的游戏吗?对此我持怀疑态度,最有可能的情况是 —— **四年做了个看上去还不错的Demo** 。虽然说这前文提到的两个开发团队花了四年的时间去制作游戏,但整个团队在游戏开发上的积累,却远不止四年,它们有着符合自身风格的游戏引擎,有着符合游戏玩法的场景规范,有着标准化的团队协作,有着长期积累的平台优化技巧,有着跟玩家长期打交道的经验,而我们,或许有不少的行业大牛,但与之相比,我们缺乏的是成熟的团队。 + +- 相信大部分开发者对游戏都怀揣着一些梦想,虽然赚钱不是我们的目的,但是却是我们必须要有的成果,投资人不是慈善家,员工不能靠爱发电。而谈到赚钱,这里有几个国内大型单机的数据参考(来源于网络),可以看出,如果除去各种抽成,宣发成本,大部分游戏都是不赚钱的: + + | 游戏名称 | 开发时间/年 | 制作成本/千万元 | 当前单价/元 | 销量/万套 | + | ------------ | ----------- | --------------- | ----------- | --------- | + | 古剑奇谭三 | 4 | 3000 | 99 | 220 | + | 嗜血印 | 4 | 4000 | 79 | 146 | + | 紫塞秋风 | 5 | 5000 | 98 | 103 | + | 仙剑奇侠传七 | 5 | 6700 | 128 | 74.2 | + | 轩辕剑柒 | 3 | 3000 | 99 | 46 | + | 河洛群侠传 | 2 | 1000 | 88 | 45.8 | + +### 游戏引擎 + +国内的计算机和游戏行业起步较晚,错过了引擎发展的黄金时期,技术探索缓慢,相关人员匮乏,所以当下大多游戏工作室都是依附于商业引擎上,如 Unreal Engine,Unity,Cocos,Godot,Cry Engine...,在一些中大厂,也有一些内部使用的自研引擎。 + +具体选择什么引擎,需要根据游戏项目的类型和体量来决定,具体可参考: + +- [[知乎] - Unity、Unreal、CryEngine这三个引擎各有什么特点?](https://www.zhihu.com/question/336750450/answer/805042145) + +本文探讨的游戏类型是大型开放世界,而 **Unreal Engine 5** 提供了 开放世界项目 相应的[工具集](https://www.unrealengine.com/zh-CN/blog/large-worlds-in-ue5-a-whole-new-open-world),使用它有着绝对的优势: + +- https://docs.unrealengine.com/5.2/zh-CN/building-virtual-worlds-in-unreal-engine/ + +![image-20230802173927033](Resources/image-20230802173927033.png) + +但 **Unreal Engine 5** 并不是万能的,它同样存在一些问题: + +- Unreal Engine 5 提供的功能是易于使用,但不易于管理的,它保证了通用,而并非无所不能,因此需要有专业的引擎团队进行调校和扩展,项目才能持续推进,可目前而言,它的一些核心功能还在迭代,这种不稳定性会给开发团队带来很大的挑战。 +- 熟练掌握UE5的(美术,策划,程序)人员是非常匮乏的,网络上散乱着各种零碎的文档和教程,这样培养出来的人往往很难在大型的团队协作中开展工作,在这种环境下,想要积累成熟的团队更是难上加难,不过好在近年官方花大气力攥写文档和技术博客,并开展了一系列培训,还有其他方面的一些协助,未来的形势会越来越好~ + - [UE 5.2 中文文档](https://docs.unrealengine.com/5.2/zh-CN/) + - [UE 技术博客](https://www.unrealengine.com/zh-CN/feed/tech-blog) + - [UE 官方培训合集](https://www.bilibili.com/video/BV1Tt4y1H7kQ) + - [UE 开发路线图](https://portal.productboard.com/epicgames/1-unreal-engine-public-roadmap/tabs/88-unreal-engine-5-3-in-progress) + - [GAMES104-现代游戏引擎:从入门到实践](https://www.bilibili.com/video/BV1oU4y1R7Km) +- Unreal Engine 是一个引擎技术的 **供应商(Supplier)** ,对于游戏中的图形要素(天空大气,地形,植被,场景模型,光照阴影,人物,布料,毛发,流体模拟...)的具体细节,并没有官方统一的标准参数或方案,UE默认是倾向于美术效果的,而在实际项目中,想要平衡项目的性能和美术效果是一件非常困难的事情。 + +### 游戏玩法 + +网络上有不少针对开放世界玩法设计的分析,笔者看过一些,给我的感觉是 ”逆向" 做的确实不错,但就有点像是:上学语文考试遇到的阅读题, 题目问“通过这段话,作者表达了什么情感?”,而我能做的,只是根据 `结果` 去编一个 让第三方看起来合理 的`过程`。 + +在 [编辑器架构](https://zhuanlan.zhihu.com/p/640337173) 这篇文章中,有提到:一个UI设计师会综合考虑界面的排版,色调,图标和布局去制作出精美的用户界面,但想要做好 **组件化设计** ,却是一件非常困难的事情,因为艺术家们往往没有开发人员那么缜密的逻辑思维。 + +一个失败的组件化设计,将会增重用户的认知负担,并且让程序的扩展和维护变得困难。 + +而在游戏项目中,一个失败的玩法设计,将会增加玩家的上手难度,导致团队的工作量激增,成本飙升,管理混乱等。 + +### 硬件资源 + +在游戏中,大量采用了实时渲染技术,因为游戏平台的计算资源是有限的,高品质的美术效果往往伴随着很大的性能损耗,对于一个游戏来说,真正对玩家有吸引力的是游戏性,美术品质只是锦上添花,而非画龙点睛。 + +因此我们需要在不影响游戏性的前提下,再去思考如何提升美术品质,做出取舍是必然的,考虑 **“取巧”** 可以让“鱼和熊掌兼得”。 + +这里有一个当下软硬件的分布情况: + +- [Steam 软件和硬件调查](https://store.steampowered.com/hwsurvey/videocard/) + +![image-20230802192946543](Resources/image-20230802192946543.png) + +可以看出,目前主流的显卡依旧是 `NVIDIA GeForce GTX 1650`,而显卡的配置就代表了游戏图形的算力,甚至决定了某个美术效果能不能使用。 + +考虑到国内玩家群体的电脑配置普遍不会太高,游戏开发团队为了争取到更多的用户,需要做很多的优化工作和兼容方案,而这会带来巨量的试错成本。 + +### 项目管理 + +笔者并非专业的项目管理人员,跟一些业内的小伙伴有过沟通,发现一些团队或多或少都存在这样的问题: + +- 工具链老旧,工作效率低下 +- 团队之间的沟通协作,会在不经意间产生一个个小的信息茧房,人员的职责划分和版本的开发周期往往是很难明确的,而这些问题如果没有处理好,会逐渐导致项目的开发流程变得混乱,甚至失控。 +- (编不动了0.0...) + +## 省钱与偷懒 + +游戏在绝大多数情况下,是一种商品,它为玩家提供精神服务,开发团队通过它获取社会价值。 + +国内不乏优秀的游戏从业人员,完全有能力制作出高品质的游戏产品,实际上,我们面临的真正难题是游戏是否能盈利。 + +游戏项目的成本和营收,由诸多因素决定,成本方面,比如流程失控,人员变动,方向调整,很容易造成成本的飙升,营收方面,比如什么IP作者辱华,运营失误,盗版横行,BUG过多,优化太烂,稍有不慎,满盘皆输。 + +本文不讨论上述因素,仅从 **技术人员** 的角度,阐述一些对于 开放世界设计 **基础建设** 的见解,其核心思想是 **优化流程,节约成本** ,其中主要围绕两个方面: + +- 基于 **Atomic** 的玩法设计 +- 基于 **Meta** 的开发流程 + +### 基于 **Atomic** 的玩法设计 + +开放的显著特征是 **一定范围内** 的自由,这种自由往往需要庞大的玩法体系来支撑,在上文我们提到,一个失败的玩法设计,将会增加玩家的上手难度,导致团队的工作量激增,成本飙升,管理混乱,那什么是一个成功的玩法设计呢? + +本节通过《塞尔达传说:王国之泪》来做简单的分析。 + +![塞尔达传说》王国之泪:我们对《荒野之息》续集的看法- Sortiraparis.com](Resources/789722-the-legend-of-zelda-tears-of-the-kingdom-breath-of-the-wild-2-bande-annonce-et-date-de-sortie.jpg) + +王国之泪的核心角色玩法如下: + +![image-20230803132314099](Resources/image-20230803132314099.png) + +- [[百度百科] 塞尔达传说:王国之泪](https://baike.baidu.com/item/%E5%A1%9E%E5%B0%94%E8%BE%BE%E4%BC%A0%E8%AF%B4%EF%BC%9A%E7%8E%8B%E5%9B%BD%E4%B9%8B%E6%B3%AA) + +从上图可以看出,王国之泪的核心玩法并不复杂,它有着非常 **低粒度** 的结构设计,而上层玩法几乎是完全建立在这个 **基础骨架** 上,从而保证了高度的资源复用。 + +再看王国之泪的怪物体系: + +![image-20230803133232216](Resources/image-20230803133232216.png) + +怪物设计同样有着 **低粒度** 的结构设计,也是在上层高度复用,比如丘丘怪,有着普通丘丘,电丘丘,火丘丘,兵丘丘,它们分布在不同的地区。 + +![456468](Resources/456468.png) + +![img](Resources/EZUU%60%5D%5D@%251HMB%7DABO%7D6PT1T.png) + +与之相似的还有: + +- 八爪怪:水八爪怪,森八爪怪,岩八爪怪,雪八爪怪,宝八爪怪 +- 蝙蝠:普通蝙蝠,电蝙蝠,火蝙蝠,冰蝙蝠 +- 岩石人:普通岩石人,熔岩人,冰岩人 +- 魔法师:电击长袍魔法师,火焰长袍魔法师,冰雪长袍魔法师 +- 古栗欧克:火焰古栗欧克,冰雪古栗欧克,雷电古栗欧克,古栗欧克王 +- ... + +还有根据怪物等级进行复用的: + +- 波克布林:普通波克布林,蓝色波克布林,黑色波克布林,骷髅波克布林,白银波克布林 +- 莱尼尔:普通莱尼尔,蓝鬃莱尼尔,白鬃莱尼尔,白银莱尼尔 +- ... + +详见: + +- [塞尔达传说王国之泪怪物图鉴](https://www.chinaz.com/2023/0518/1525643.shtml) + +再看烹饪玩法: + +- 食品被划分为 水果类,蘑菇类,肉类,鱼类,蔬菜类,结合调料可以制作菜肴,怪物碎片和昆虫可以制作药材,开发者根据这些大类制作了 烹饪 **模板(Template)** ,比如任意 `蘑菇类食品` 和 `水果类食品` 放一起烹饪,得到是 `水果拌蘑菇`,然后取小类上的一些物品 **生成特定(Specialize)** 的菜肴,比如 `塔邦挞小麦+山羊奶油+生命鲑鱼=干煎鲑鱼`,这类物品可以影响角色的状态(体力,精力,Buff...),甚至推进游戏进度的发展。 + +> 具体的烹饪公式,详见 [塞尔达传说王国之泪料理配方大全一览](https://www.9game.cn/sedcswgzl/8089996.html) + +综上,不难看出,王国之泪的核心玩法是一种 [ **自顶向下** ](https://zhuanlan.zhihu.com/p/77479952) 的设计方式,就像是这样: + +![image-20230803141535141](Resources/image-20230803141535141.png) + +而上层玩法,则是在核心玩法的基础上, **自底向上进行组合** ,其中的重要手段是:在抽象级别制作 **模板(Template)** ,实例级别处理 **特化(Specialize)** ,就像是这样: + +![image-20230803153605584](Resources/image-20230803153605584.png) + +而这一设计理念,有点类似于UI设计中的原子设计理论,这里有一本非常非常非常好的相关书籍: + +- [[知乎] 《原子设计》全书知识架构 - 加西莫多](https://zhuanlan.zhihu.com/p/137173868) + +它的目录结构如下: + +![image-20230803144252164](Resources/image-20230803144252164.png) + +虽然这是一本介绍UI设计的书籍,但它的很多理念特别适用于开放世界的玩法设计。 + +遵循原子设计理念可以尽可能的复用项目资源,让开放流程更清晰直观,此外,还需要强调一点的是: + +- **在开发初期,就应该明确开放世界的核心玩法设计** + +因为后续的场景规范和逻辑开发都是围绕着核心玩法进行的,还是以塞尔达为例,它的场景元素有: + +![image-20230803151941438](Resources/image-20230803151941438.png) + +在明确核心角色玩法之后,开发团队才能制定与之对应的 **场景规范** ,确立 **性能优化指标** ,并制作相应的 **工具链** 。 + +例如这样的规范: + +- 根据角色的精力(游泳,攀爬,滑翔)机制,去定制地形高度,坡度和水体的规范。 +- 根据通天术的技能要求,去定制地形规范。 +- 根据究极手,余料建造,倒转乾坤这些技能的特性,去制作符合其要求的可交互物件。 +- 武器(单手剑,双手剑,长枪,盾牌,箭)可以进行余料建造,在对应的插槽上进行特性绑定,来制定余料物品的规范。 +- 已知角色的状态类别,可以去制作不同的场景效果,比如雷击,着火,寒冷,炎热,并为之提供相应的解决方案(服装 or 料理)。 +- 在没有与核心玩法设计规范出现冲突的前提下, 制作左纳乌科技的玩法。 + +针对这些规范,引擎开发人员可以进行类似如下的开发工作: + +- 定制`地形可攀爬高度`,`水体可穿越区域`,`通天术可使用区域` 的调试视图(如果UE支持不改源码,能以插件的形式扩展调试视图就很舒服)。 +- 验证各种特效,场景效果的性能指数,调校效果与性能的平衡,并制作相应的管理和验收工具。 + +上述内容虽然不是游戏的重心,但如果这些 **基础建设** 没有做好,将会是流程混乱的开端。 + +### 基于 **Meta** 的开发流程 + +关于Meta,笔者在[编辑器架构](https://zhuanlan.zhihu.com/p/640337173)一文中有所提及,Meta思维在现代工业软件开发过程中可以提供极大的便捷度,它能完成一劳永逸的代码扩展方式。 + +> 关于 meta 思维的联想词有:`求导`,`高阶维度`,`上帝视角`,... +> 详见: [[ 知乎 \] bus waiter- 谈meta](https://zhuanlan.zhihu.com/p/406257437) + +假如将写代码比作盖房子,我们考虑的是如何盖好房子,那在Meta层面,我们考虑的是如何构建一台自动盖房子的机器,Meta思维让我们从一个工程问题转换成了另一个,虽然难度有所上升,但只要方法得当,它能带来无可比拟的收益。 + +在传统的开发流程中,我们思考的是 **如何构建具体的产品** ,而Meta的流程下,我们思考的是 **如何搭建产品的生产管线** ,其关键的思考维度是: **操作也是数据(Operation is also data)** ,相较于具体产品, **管线是易于对比,积累和迭代的** 。 + +Meta思维为工业化的流程提供了极大的便捷,试想一个这样的场景: + +- 你是一个勤勤恳恳的UE小美术,某天,你突然收到一个大佬那边传来的一个需求:要为某个场景铺上植被。你一想,这我熟啊,立马整了几个植被模型,打开UE5的植被模式,用 画刷 咔咔几下,就把植被给刷好了,叫来大佬一看,大佬说:“你这不行啊,一点意境都没有,再改一版。”,然后你绞尽脑汁,废寝忘食,折腾好几天,终于是搞完了,前后一对比,确实好了不少,叫来大佬再看,大佬也是连连称赞,然后这事也就翻篇了,直到某个风雨交加的下午,一个自称是引擎开发工程师的小秃子带着质问的语气问你:“这片植被是你做的吗?”,你只得颤颤巍巍到:“是...是啊,怎么啦?”,“现在场景的复杂度变高了,植被这里的性能消耗很高,植被模型面数也比较高,并且刷得太密了”,然后啪啪摆出一堆性能测试结果作为证据,让你进行优化,可是如果要优化的话,就得重新再刷一遍,又是几天繁琐的工作量,于是你找到大佬,渴望大佬救自己于水火之中,大佬说:“改吧,注意形态效果要跟之前差不多。”,听闻此言,宛若晴天霹雳,但为了保住这份工,你无奈只能照做,浑浑噩噩几天,终于熬了出来,叫来小秃子一看,确实少了不少,于是它心满意足的离开了,结果第二天,这个*又回来了,说是优化的还不够,还得减,你一听,怒发冲冠,一脚踹在小秃子身上,看着它躺在地上嚎啕大哭的样子,你仰天大笑,笑着笑着睁开了双眼,挣扎着从电脑桌前支棱起来,擦去嘴角的口水,继续加班~ + +回顾两位主角,他们自身都存在一定问题: + +- 美术人员 只关注艺术效果,对性能的关注度有所缺失。 +- 引擎人员 没有提前定制好规范,对逐渐复杂的工程没有做好及时的管控。 + +但真的是他们的问题吗? + +- 难道美术人员还需要深入了解图形学和计算机硬件? +- 引擎人员真的有能力第一时间把控整个团队的开发动向? + +很显然,我觉得并不完全是他们的问题,因为这些问题已经超出了他们的能力范畴,但在流程上,我们可以通过一些工具和策略,来规避此类问题: + +- 美术制作流程上的 **程序化(Procedural)** +- 引擎管控流程上的 **自动化(Automation )** + +#### 程序化(Procedural) + +[程序化(Procedural)](https://en.wikipedia.org/wiki/Procedural_generation)也叫过程化,它指特定问题的求解过程。程序化应用的本质上是将问题的求解过程当作是数据资产进行存储,从而保证后续开发能够依靠这些操作数据,进行复用,迭代等(在概念上,它就是一个程序,只不过是在程序中的程序)。 + +游戏行业当下对程序化的应用主要有: + +- [ **程序化内容生成(PCG,Procedural Content Generation)** ](https://docs.unrealengine.com/5.2/en-US/procedural-content-generation-overview/) +- [ **程序化建模** ](https://en.wikipedia.org/wiki/Procedural_modeling) +- [ **程序化纹理** ](https://en.wikipedia.org/wiki/Procedural_texture) +- [ **程序化动画** ](https://en.wikipedia.org/wiki/Procedural_animation) +- [ **程序化音频** ](https://en.wikipedia.org/wiki/Synthetic_media) + +而这些技术,其实已经逐渐渗透到了美术资产制作的日常当中,它的显著特征是: **通过编程来制作资产而非只有调参** 。 + +以Unreal Engine为例,它有着优秀的程序化支持: + +- [Procedural Content Generation](https://docs.unrealengine.com/5.2/en-US/procedural-content-generation-overview/) :虚幻中用于PCG的工具集,它为技术美术师、设计师和程序员提供了构建快速、迭代工具和任何复杂性内容的能力,范围从资产(例如建筑物或生物群落生成)到整个世界。 +- [Meta Sounds](https://docs.unrealengine.com/5.2/en-US/metasounds-in-unreal-engine/) :支持进行用户自定义、第三方可扩展、图表重复利用,并提供了可以在编辑器中进行声音设计的强大工具。 +- [Geometry Script](https://docs.unrealengine.com/5.2/zh-CN/introduction-to-geometry-scripting-in-unreal-engine/):虚幻插件,提供了一组使用蓝图和Python生成和编辑网格体几何体的,它可以在编辑器工具控件( Editor Utility Widgets)中编写几何体脚本(Geometry Scripting)和 资产操作(Asset Actions)来创建自定义网格体分析函数库、处理和编辑工具,还可以在Actor蓝图中用它创建程序化对象并实现复杂的几何查询。 +- [Animation Blueprints](https://docs.unrealengine.com/5.2/en-US/animation-blueprints-in-unreal-engine/):用于模拟或在游戏中控制骨骼网格体的蓝图,可以在其中混合动画,调整骨架,创建逻辑来定义每帧使用的最终动画姿势(Pose) +- [Material Blueprint](https://docs.unrealengine.com/5.2/en-US/unreal-engine-materials/):用于定义场景对象表面属性的蓝图,可以使用各种图像(纹理),基于节点的材质表达式 以及 材质本身的固有属性 来定义 场景物体最终的表面属性。 + +对于PCG框架,笔者建议深入学习官方的 《Electric Dreams》项目,以及一些相关资料: + +- [『UE5 PCG』官方公开课:深入研究Electric Dreams环境项目](https://www.bilibili.com/video/BV1y8411U7Hs) +- [Electric Dreams PCG技术详解(一)——术语、工具和图表介绍](https://mp.weixin.qq.com/s/mGWiPKvWU_NHTktIWA6LnA) +- [Electric Dreams PCG技术详解(二)——沟壑、大型Assembly](https://mp.weixin.qq.com/s/xHdnrFTkywF_7OjqOuALbw) +- [Electric Dreams PCG技术详解(三)——森林、岩石和场景背景](https://mp.weixin.qq.com/s/YWcd8l-VphNLrOlg3zkSzA) +- [Electric Dreams PCG技术详解(四)——远景、雾气、自定义节点及子图表](https://mp.weixin.qq.com/s/CWTUcRIBTXw8WvhX3jrcBw) + +#### 自动化(Automation ) + +[自动化(Automation )](https://en.wikipedia.org/wiki/Automation)描述了一系列减少流程中人为干预的技术,即通过预先确定决策标准、子流程关系和相关操作,以及在机器中体现这些预先确定。自动化可以通过多种方式实现,通常是组合使用。复杂的系统,例如现代工厂、[飞机](https://en.wikipedia.org/wiki/Airplane)和船舶通常使用所有这些技术的组合。自动化的好处包括节省劳动力、减少浪费、节省[电力](https://en.wikipedia.org/wiki/Electricity)成本,节省材料成本,并提高质量、准确性和精确度。 + +在软件系统开发中,自动化技术常见用法是:[自动化测试与打包发布](https://en.wikipedia.org/wiki/Test_automation)。 + +在游戏开发中,自动化的流程对中大型团队而言至关重要,它的优势在于 **易于迭代和积累** 。 + +在UE5开放世界项目中,自动化的有着很多的用武之地: + +- 自动化的打包发布流程 +- 性能和效果平衡方案的自动化测试(光影,HLOD,地形,植被等各种优化策略) +- 自动化验证和验收流程 +- ... + +在[《Fortnite》的官方演讲](https://www.bilibili.com/video/BV1aL411m7xs/?share_source=copy_web&vd_source=dfd2f1bf643a3e0722141847666af060&t=409)中,简单提到了他们是如何通过自动化来探索出真正高效的树木策略。 + diff --git "a/Docs/05-Other/1.\345\205\203\345\256\207\345\256\231\346\235\202\350\260\210.md" "b/Docs/05-Other/1.\345\205\203\345\256\207\345\256\231\346\235\202\350\260\210.md" new file mode 100644 index 00000000..4e74820c --- /dev/null +++ "b/Docs/05-Other/1.\345\205\203\345\256\207\345\256\231\346\235\202\350\260\210.md" @@ -0,0 +1,207 @@ +## 背景 + +这周被抓到上海参加集训,有个游戏相关的研究汇报,本来想着通过脑袋瓜里的一些硬通货,秀下肌肉,结果一上台就猿形毕露,重点重点没说,还尬住好几回,白瞎了组内小伙伴的协助以及连夜赶的PPT,果然我目前还是只能写点技术小作文。 + +> 长期的闭门造车,导致了口述能力很差,思维跳脱,情商又低,这次可以说是充满遗憾~ + +很多同学了解到 **元宇宙(Metaverse)** ,可能是从电影《头号玩家》所呈现出来的效果,也有来自于扎克伯格对于元宇宙的阐述,但实际上,元 这个概念,在人类近代社会发展中,已经有了非常成熟的应用。 + +那什么是元呢? + +## 元 + +元 转译自英文单词 **Meta** ,正如其他单词一样,我们造词,是为了 **归纳和总结** 现实世界中 `事` 或 `物` 及其`行为` 和 `特征`,从而更易于知识 的 **传播** 。 + +Meta也一样,它用于描述 一件事物在 **发展过程中** 触发 **“进化”** 的 **本质** 。 + +这么说可能有点抽象,我给大家举个例子: + +假如我想从深圳,去到北京,放到一万年前,大家会以什么方式去实现呢? + +很显然,在当时的我们能想到的只有徒步。 + +在一千年以前呢? + +我们可能会骑着马过去。 + +那现在呢? + +哦,已经有了非常多的选择,车,火车,高铁,飞机。 + +那这些跟Meta有什么关系呢? + +我想表达的是:Meta其实是一种思维方式的转变,当我们放弃徒步这种直接的行为,去思考,咦,我们能不能通过什么工具来帮助我们去达成目标的时候,它就是Meta的体现。 + +为了让大家能够对Meta有更清晰的认知,这里我斗胆给Meta下个定义: + +- Meta是指思维方式的跃迁,在Meta层面,我们考虑的 **不是** **具体的产品** 和 **实际的目标** ,而是 **转而去思考** 如何 **搭建产品的生产管线** 和 **构建达成目标的中间工具** 。 + +说到这里,可能还有同学对Meta的概念有些模糊,那我再举一个例子: + +假如我们现在面临一个问题: + +- 想要修建一栋大楼 + +通常情况下,我们会想到比较直接的行为,撸起袖子直接干。 + +但在Meta层面,我们会思考如何去构建一个自动修建大楼的机器。 + +甚至我们的思维可以进一步跃迁,去思考如何搭建一个生成自动盖楼机的工厂。 + +![ZAasdg](Resources/ZAasdg.gif) + +这个例子可能有些虚幻,但只要稍加思索,相信很快就能明白Meta的概念。 + +那Meta能够我们带来什么呢? + +- 它可以帮助我们制造出 **一劳永逸** 并且 **可持续性迭代** 的产物。 + +> 比如说车,一个人发明,所有人都长期受益。 +> +> 而交通工具从马,到车,到火车,到高铁,飞机也体现了可持续性的迭代 + +在人类社会的发展中,Meta的思维体现在方方面面,举两个例子: + +- 在工业上,人们从原始社会的鲁莽行事,迈向了工业社会,通过制作工具来提升生产效率,而现在,工业自动化已经初具规模。 +- 在学习上,人们从一开始的填鸭式学习,到学习如何学习,再到现在,自动学习机器已经有了具象化的体现(AI),甚至已经能做到让AI去学习如何学习。 + +在了解了 **元(Meta)** 的概念之后,不难发现,当我们Meta的维度愈高,所面临的问题也就越难,但它所带来的收益也越大 + +从这个维度去审视元宇宙,大家可能已经有了一些不同以往的切入点,它其实也是Meta的产物,来自于我们一种这样的愿景: + +- **真实的世界一片混沌,但在元宇宙中,我们无所不能(The real world is chaotic, but we can control the metaverse)** + +## 元宇宙 + +在人们掌控 Meta 这一神器之后,思维的跃迁变得一发不可收拾,元宇宙的出现是必然的。 + +但目前而言,大众对元宇宙的定义被混淆成了一个具有如下特征的游戏: + +- `开放世界`+`MMO`+`VR/AR` + +这个概念完全沦为了炒作,骗钱的工具,什么元宇宙买房,元宇宙工作,元宇宙会议,元宇宙逛街...,相信大多数懂技术的小伙伴都很难绷得住。 + +知乎上有一个相关讨论: + +- [为什么我觉得元宇宙是个骗局? - 知乎](https://www.zhihu.com/question/486678291) + +[这个回答](https://www.zhihu.com/question/486678291/answer/2266323732)直戳痛点: + +![image-20231029100137554](Resources/image-20231029100137554.png) + +我们要明白的是,当下的算力和生态,还不足以支撑起元宇宙的构建。 + +**黑客帝国(The Matrix)** 对我们目前来说,遥不可及: + +![matrix](Resources/matrix.jpg) + +**头号玩家(Ready Player One)** 让我们寻得了一丝希望: + +![头号玩家 (2018)](Resources/webstory-rpt-movie-12.jpg) + +- https://www.denofgeek.com/games/ready-player-one-easter-eggs-references-movie-guide-complete/ + +虽然真正意义上的元宇宙对我们来说非常遥远,但当下的技术储备,也足以支撑我们在一个虚拟世界中,完成很多有趣的事情。 + +游戏行业一直处在相关领域的前沿,从它的发展情况我们可以一窥当下技术能力的上限: + +![The Matrix Awakens: An 'Unreal' experience worth trying | Daily Sabah](Resources/168519.jpg) + +> [Unreal Engine 5 《黑客帝国:觉醒》 (2022.2)](https://www.bilibili.com/video/BV1rY411p7Tg) + +根据当前的技术优势,找准元宇宙发展的定位: + +- **直追真实世界的画面效果** +- **可以实现 现实中不存在 或者说 高成本 的 吸引人但方式简单 的 交互体验** +- **自由 — 不受物理规则和思维方式的束缚** + +避免走入到一些陷阱之中: + +- **一味地想要把现实的东西搬到元宇宙** +- **甩不开社会体系和游戏设计的包袱** +- **当前算力还不足以支撑下一代社交产品的出现** + +这样我们才能做出一些真正有实际价值的产品。 + +### 落地方向 + +#### 真实场景复刻 + +- 文旅,文博宣传,导航 + +网易赛车游戏《巅峰极速》中,复刻白云山作为赛道: + +![image-20231029111550741](Resources/image-20231029111550741.png) + +在 Unreal Engine 5 的加持下,可以轻易实现超大场景的漫游: + +![ZAa2g](Resources/ZAa2g.gif) + +像和平精英那样在这种鸟瞰镜头下增加一些辅助UI: + +image-20231029113453727 + +这可能就是下一代的导航地图。 + +再在局部增加一些引导UI和创意元素,让AR设备能够同步: + +image-20231029115610425 + +> 视频:[新加坡AR旅游](https://www.youtube.com/watch?v=zFxpXiAkT2k) + +文旅宣传的价值就体现出来了 + +#### 沉浸式体验 + +- 演唱会,运动会,艺术类会展 + +笔者目前并没有发现关于这方面比较好的落地产品,很多产品会把它们在 元宇宙中的发展重点 聚焦在 **社交** 和 **互动** 上面,但正如前文所述,当下的算力还支撑不起下一代的社交产品,当下的技术积累还做不到复杂的交互体验,强行去推进最终得到的只能是一堆烂泥。 + +但现在我们可以依托图形学的高速发展,在元宇宙中去 **扩大艺术的呈现** 。 + +许多小伙伴可能会看过一些灯光秀或者电子音乐节,它们所带来的视听体验是非常震撼的: + +![image-20231029121624602](Resources/image-20231029121624602.png) + +> 视频:[大疆无人机灯光秀(2023年10月16日 世界粮食日)](https://www.bilibili.com/video/BV1AQ4y1s7D3) + +image-20231029123354005 + +> 视频:[拉斯维加斯球型巨幕 MSG Sphere(造价23亿美元)](https://www.bilibili.com/video/BV1Vw411C7Rd) + +它们受限于硬件设施,但在虚拟世界中,我们可以做得更好,更多: + +![image-20231029124516544](Resources/image-20231029124516544.png) + +> 视频:[音乐图形艺术](https://www.bilibili.com/video/BV1iy4y167pf/) + +像是堡垒之夜的虚拟演唱会: + +![image-20231029123217249](Resources/image-20231029123217249.png) + +还可以结合 **VR/AR** 来增加更多的代入感: + +image-20231029114554495 + +> 视频:[特效钢琴 / PianiCast](https://www.bilibili.com/video/BV1fZ4y1P7ue) + +### 技术支撑 + +这里笔者简单画了一下目前元宇宙的部分技术体系(灵魂画家) + +image-20231029131315036 + +算力是土壤,为上层的树木提供能量,并链接起了一片片森林。 + +工业化能力是树的根,干和枝,它有效利用从土壤中获取的能量,支撑起树的整个形态,并延展出一条条的分支。 + +技术,内容和产品分别对应树的叶,花和果实。 + +AI像是雨水,它能够丰富内容和玩法,这是大众接触最多的一点,比如最近很火的Chat GPT,各类AIGC的应用,但更为重要的是,它还能滋润算力的土壤,另辟蹊径,让图形算力能够在一定程度上突破硬件的限制。 + +工业化能力是很多公司所欠缺的点,它主要体现在: + +- **技术的把控和融合** :`技术不懂产品,产品不懂技术` 基本上是多数人或公司的真实写照,我们要明确的是,当前的技术积累是有上限的,所以我们考虑的首要目标不是设计,而是做好各类资源(时间,精力,金钱, **算力** )预算的分配, **不能即要又要** ,某个产品里有个好点子,搬!某个效果不错,抄!缺乏严谨的底层架构设计,将会导致整个制作管线的混乱。 +- **Meta的应用** :我们知道一个人的能力是有限的,所以我们可能会将希望寄托于一个团队,但在一个超大型的系统构建中,所面临的已经不仅仅只是一个技术问题了,它还是一个工程问题,我们得清晰地认识到, **人的能力是有限的** ,所以我们都借助工具,在技术方面,Meta的应用主要体现在:制作流程上的 **程序化(Procedural)** ,管控流程上的 **自动化(Automation )** + +关于这方面的细节,笔者有一些见解,但不成体系,缺乏打磨,之后再分享出来。 diff --git a/Docs/05-Other/Resources/168519.jpg b/Docs/05-Other/Resources/168519.jpg new file mode 100644 index 00000000..e0943fb4 Binary files /dev/null and b/Docs/05-Other/Resources/168519.jpg differ diff --git a/Docs/05-Other/Resources/456468.png b/Docs/05-Other/Resources/456468.png new file mode 100644 index 00000000..c72d8e44 Binary files /dev/null and b/Docs/05-Other/Resources/456468.png differ diff --git a/Docs/05-Other/Resources/789722-the-legend-of-zelda-tears-of-the-kingdom-breath-of-the-wild-2-bande-annonce-et-date-de-sortie.jpg b/Docs/05-Other/Resources/789722-the-legend-of-zelda-tears-of-the-kingdom-breath-of-the-wild-2-bande-annonce-et-date-de-sortie.jpg new file mode 100644 index 00000000..d48b3086 Binary files /dev/null and b/Docs/05-Other/Resources/789722-the-legend-of-zelda-tears-of-the-kingdom-breath-of-the-wild-2-bande-annonce-et-date-de-sortie.jpg differ diff --git a/Docs/05-Other/Resources/EZUU`]]@%1HMB}ABO}6PT1T.png b/Docs/05-Other/Resources/EZUU`]]@%1HMB}ABO}6PT1T.png new file mode 100644 index 00000000..75bf0580 Binary files /dev/null and b/Docs/05-Other/Resources/EZUU`]]@%1HMB}ABO}6PT1T.png differ diff --git a/Docs/05-Other/Resources/ZAa2g.gif b/Docs/05-Other/Resources/ZAa2g.gif new file mode 100644 index 00000000..8d369da7 Binary files /dev/null and b/Docs/05-Other/Resources/ZAa2g.gif differ diff --git a/Docs/05-Other/Resources/ZAag.gif b/Docs/05-Other/Resources/ZAag.gif new file mode 100644 index 00000000..77be2803 Binary files /dev/null and b/Docs/05-Other/Resources/ZAag.gif differ diff --git a/Docs/05-Other/Resources/ZAasdg.gif b/Docs/05-Other/Resources/ZAasdg.gif new file mode 100644 index 00000000..a4f9c1ab Binary files /dev/null and b/Docs/05-Other/Resources/ZAasdg.gif differ diff --git a/Docs/05-Other/Resources/image-20230801100500180.png b/Docs/05-Other/Resources/image-20230801100500180.png new file mode 100644 index 00000000..7cbce64b Binary files /dev/null and b/Docs/05-Other/Resources/image-20230801100500180.png differ diff --git a/Docs/05-Other/Resources/image-20230801100516172.png b/Docs/05-Other/Resources/image-20230801100516172.png new file mode 100644 index 00000000..27f0ff01 Binary files /dev/null and b/Docs/05-Other/Resources/image-20230801100516172.png differ diff --git a/Docs/05-Other/Resources/image-20230802173927033.png b/Docs/05-Other/Resources/image-20230802173927033.png new file mode 100644 index 00000000..b045867a Binary files /dev/null and b/Docs/05-Other/Resources/image-20230802173927033.png differ diff --git a/Docs/05-Other/Resources/image-20230802192946543.png b/Docs/05-Other/Resources/image-20230802192946543.png new file mode 100644 index 00000000..33d61abe Binary files /dev/null and b/Docs/05-Other/Resources/image-20230802192946543.png differ diff --git a/Docs/05-Other/Resources/image-20230803132314099.png b/Docs/05-Other/Resources/image-20230803132314099.png new file mode 100644 index 00000000..74da9b51 Binary files /dev/null and b/Docs/05-Other/Resources/image-20230803132314099.png differ diff --git a/Docs/05-Other/Resources/image-20230803133232216.png b/Docs/05-Other/Resources/image-20230803133232216.png new file mode 100644 index 00000000..88918d84 Binary files /dev/null and b/Docs/05-Other/Resources/image-20230803133232216.png differ diff --git a/Docs/05-Other/Resources/image-20230803141535141.png b/Docs/05-Other/Resources/image-20230803141535141.png new file mode 100644 index 00000000..6fd90569 Binary files /dev/null and b/Docs/05-Other/Resources/image-20230803141535141.png differ diff --git a/Docs/05-Other/Resources/image-20230803144252164.png b/Docs/05-Other/Resources/image-20230803144252164.png new file mode 100644 index 00000000..d3e929e1 Binary files /dev/null and b/Docs/05-Other/Resources/image-20230803144252164.png differ diff --git a/Docs/05-Other/Resources/image-20230803151941438.png b/Docs/05-Other/Resources/image-20230803151941438.png new file mode 100644 index 00000000..b3dfbd18 Binary files /dev/null and b/Docs/05-Other/Resources/image-20230803151941438.png differ diff --git a/Docs/05-Other/Resources/image-20230803153605584.png b/Docs/05-Other/Resources/image-20230803153605584.png new file mode 100644 index 00000000..d536db96 Binary files /dev/null and b/Docs/05-Other/Resources/image-20230803153605584.png differ diff --git a/Docs/05-Other/Resources/image-20231029100137554.png b/Docs/05-Other/Resources/image-20231029100137554.png new file mode 100644 index 00000000..012867a2 Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029100137554.png differ diff --git a/Docs/05-Other/Resources/image-20231029111550741.png b/Docs/05-Other/Resources/image-20231029111550741.png new file mode 100644 index 00000000..ed33ec77 Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029111550741.png differ diff --git a/Docs/05-Other/Resources/image-20231029113453727.png b/Docs/05-Other/Resources/image-20231029113453727.png new file mode 100644 index 00000000..56d622e0 Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029113453727.png differ diff --git a/Docs/05-Other/Resources/image-20231029114554495.png b/Docs/05-Other/Resources/image-20231029114554495.png new file mode 100644 index 00000000..8394dbae Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029114554495.png differ diff --git a/Docs/05-Other/Resources/image-20231029115610425.png b/Docs/05-Other/Resources/image-20231029115610425.png new file mode 100644 index 00000000..50b3126d Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029115610425.png differ diff --git a/Docs/05-Other/Resources/image-20231029121624602.png b/Docs/05-Other/Resources/image-20231029121624602.png new file mode 100644 index 00000000..eca46a7b Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029121624602.png differ diff --git a/Docs/05-Other/Resources/image-20231029123217249.png b/Docs/05-Other/Resources/image-20231029123217249.png new file mode 100644 index 00000000..4aa81a8a Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029123217249.png differ diff --git a/Docs/05-Other/Resources/image-20231029123349042.png b/Docs/05-Other/Resources/image-20231029123349042.png new file mode 100644 index 00000000..5b022c1f Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029123349042.png differ diff --git a/Docs/05-Other/Resources/image-20231029123354005.png b/Docs/05-Other/Resources/image-20231029123354005.png new file mode 100644 index 00000000..5b022c1f Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029123354005.png differ diff --git a/Docs/05-Other/Resources/image-20231029124516544.png b/Docs/05-Other/Resources/image-20231029124516544.png new file mode 100644 index 00000000..8788ba8d Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029124516544.png differ diff --git a/Docs/05-Other/Resources/image-20231029131315036.png b/Docs/05-Other/Resources/image-20231029131315036.png new file mode 100644 index 00000000..28874a3e Binary files /dev/null and b/Docs/05-Other/Resources/image-20231029131315036.png differ diff --git a/Docs/05-Other/Resources/matrix.jpg b/Docs/05-Other/Resources/matrix.jpg new file mode 100644 index 00000000..4d70c782 Binary files /dev/null and b/Docs/05-Other/Resources/matrix.jpg differ diff --git a/Docs/05-Other/Resources/webstory-rpt-movie-12.jpg b/Docs/05-Other/Resources/webstory-rpt-movie-12.jpg new file mode 100644 index 00000000..171cb506 Binary files /dev/null and b/Docs/05-Other/Resources/webstory-rpt-movie-12.jpg differ diff --git a/Docs/README.md b/Docs/README.md index cbca5a09..3a66d529 100644 --- a/Docs/README.md +++ b/Docs/README.md @@ -4,15 +4,7 @@ comments: true # 开篇 -笔者一路走来,感慨万千... - -当下的技术环境,培养了一大批 **徘徊在门口** 的学者和一部分 **不食人间烟火** 的高玩,前者求道无门,后者工程能力欠佳。 - -笔者尝试攥写这个系列的文章,指明主干的技术路线,填补理论到实践的断层,希望能够在未来几年,拔高技术素养,让更多的小伙伴参与进来,减轻工作压力,摆脱自己加班填坑的厄运... - -## 关于文章 - -早在去年,笔者就有制作本系列文章的计划,在接触到几位大牛之后,才认识到自己认知的浅薄,因此花了一年的时间,不断精进,反复打磨,文章的标题,也从一开始所谓的的 **课程** ,变成了 **教程** ,最后换成了 **指南** ,在发布之时,思来想去,加上 **入门** 二字才觉得更妥当一些,也对,这并不是一个细致入微的教程,也没有包含太多的高级篇章,这些文章的目的不是为了让读者走捷径,,而是为了让初学者 **少走弯路** ,把更多的精力放在更高级的理论技术上面,在此之前,读者至少要达到一个这样的境界 — **I can do anything if I want** +该系列文章旨在归纳现代图形引擎开发必备的基本技术路线,它并不是一个细致入微的教程,也没有包含太多的高级篇章,这些文章的目的不是为了让读者走捷径,,而是为了让初学者 **少走弯路** ,把更多的精力放在更高级的理论技术上面,在此之前,读者至少要达到一个这样的境界 — **I can do anything if I want** 在这些文章中,你能学到: @@ -22,26 +14,14 @@ comments: true - **现代化的图形API** :是Vulkan,还是DX12、Metal呢?哦,都不是,但包您满意,拭目以待。 -本系列文章适用于有志于从事C++开发的学生,主要技术路线是图形和引擎,对于其他相关方向,前面一些章节也具有参考意义。 - -> 无论之后的目标是自研引擎还是商业引擎,这个系列文章都能让你以更少的时间成本得到更多的学习收益。 -> -> 大概需要多久呢?— 几年吧 -> -> 否则呢?— Maybe Never +- **Unreal Engine 5** -文章主要以文字为主,讲解学习路线的主干以及一些容易被忽略的知识盲区,后续实践部分通过视频演示。 +本系列文章适用于有志于从事C++开发的学生,主要技术路线是图形和引擎,对于其他相关方向,前面一些章节也具有参考意义。 -## 关于笔者 +文章主要以文字为主,讲解学习路线的主干以及一些容易被忽略的知识盲区,后续实践部分通过视频演示,文章的篇幅不会很大,笔者会尽可能的保证质量和更新速度,争取一年左右完结。 **讨论Q群:128731454** -讨论群用于给大家提供一个在学习中互帮互助的社群,大家在学习的过程中,要敏而好学,不耻下问... - -笔者在职于国内某不知名游戏工作室,由于项目工期比较紧张,可能很少有时间来一对一解答大家的疑惑, - -对于文章,篇幅不会很大,笔者尽可能的保证质量和更新速度,争取一年左右完结。 - ## 章节目录 - C++ @@ -53,33 +33,30 @@ comments: true - [x] [GUI](00-C++/6.GUI.md) - 图形 API - [x] [概述](01-GraphicsAPI/0.概述.md) - - [x] [渲染窗口](01-GraphicsAPI/1.渲染窗口.md) - - [x] 图形渲染管线 - - [x] 纹理 - - [x] 实例化 - - [x] 多渲染目标 - - [x] 离屏渲染 - - [x] 几何着色器 - - [x] 计算管线 - - [x] 间接渲染 + - [x] [基础](01-GraphicsAPI/1.基础.md) + - [x] [图形渲染管线](01-GraphicsAPI/2.图形渲染管线.md) + - [x] [着色器](01-GraphicsAPI/3.着色器.md) + - [x] [纹理与缓冲区](01-GraphicsAPI/4.缓冲区与纹理.md) + - [x] [3D空间](01-GraphicsAPI/5.三维空间.md) + - [x] [图形渲染进阶](01-GraphicsAPI/6.图形渲染进阶.md) - 引擎技术 - - [x] 渲染架构 - - [x] 编辑器 - - [x] DebugDraw + - [x] [渲染架构](02-EngineTechnology/0.渲染架构.md) + - [x] [编辑器架构](02-EngineTechnology/1.编辑器架构.md) + - [ ] DebugDraw - [ ] GPU 调试 - - [ ] 数字信号处理 - - [ ] 视频渲染 + - [x] 数字信号处理 + - [x] 视频渲染 - [ ] 插件 - [ ] 脚本 - 图形技术 - - [ ] 样条线 - - [ ] 文字渲染 + - [x] 样条线 + - [x] 文字渲染 - [x] 天空盒 - [x] 静态网格体 - [x] 骨骼网格体及骨骼动画 - [x] GPU粒子 - - [ ] Shader Toy + - [x] Shader Toy - [ ] 后期滤镜 - [x] 模糊(Blur) - [x] 泛光(Bloom) @@ -90,16 +67,25 @@ comments: true - [ ] 屏幕空间折射(Screen Space Refraction) - [ ] 流动图(Flow Mapping) - [ ] 描边(Outlining) - - [ ] 景深(Depth Of Field) + - [x] 景深(Depth Of Field) - [ ] 分色(Posterization) - [ ] 像素化(Pixelization) - [ ] 锐化(Sharpen) - [ ] 光照篇 - - [ ] 延迟着色法 - - [ ] Phone光照模型 - - [ ] PBR光照模型 - - [ ] IBL + - [x] 延迟着色法 + - [x] Phone光照模型 + - [x] PBR光照模型 + - [x] IBL - [ ] ShadowMap - [ ] 地形 - [ ] 体积雾 - [ ] 天空大气 +- Unreal Engine 5 + - [x] [基础编程](04-UnrealEngine/0.基础编程.md) + - [x] [Slate开发](04-UnrealEngine/1.Slate开发.md) + - [ ] [插件开发](04-UnrealEngine/2.插件开发.md) + - [x] [粒子系统](04-UnrealEngine/3.粒子系统.md) + - [x] [Niagara 性能优化 ](04-UnrealEngine/4.Niagra性能优化.md) + - [x] [Fluid Ninja 流体插件 ](04-UnrealEngine/5.FluidNinja流体插件.md) + - [x] [开发世界制作](04-UnrealEngine/6.开放世界制作.md) + - [x] [音频开发](04-UnrealEngine/6.音频开发.md) diff --git a/Docs/Stylesheet/mkdocs_extra.css b/Docs/Stylesheet/mkdocs_extra.css index 2a95b731..cf84af1e 100644 --- a/Docs/Stylesheet/mkdocs_extra.css +++ b/Docs/Stylesheet/mkdocs_extra.css @@ -49,11 +49,11 @@ } .md-typeset img { - box-shadow: 1px 5px 10px 1px #898989; + box-shadow: 1px 5px 10px 1px #00000089; border-radius: 0px; border: 0px; height: auto; - max-width: 60%; + max-width: 80%; margin-left: auto; margin-right: auto; display: block; @@ -85,26 +85,26 @@ @media screen and (min-width:100em) { html { - font-size: 125.0% + font-size: 100.0% } } @media screen and (min-width:125em) { html { - font-size: 150% + font-size: 120% } } .md-grid { margin-left: auto; margin-right: auto; - max-width: 80rem + max-width: 70rem } -/* body { +body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; --md-text-font-family: var(--md-text-font, _), -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; --md-code-font-family: var(--md-code-font, _), SFMono-Regular, Consolas, Menlo, monospace; - text-shadow: 0px 0px 2px #AAAAAA -} */ \ No newline at end of file + /* text-shadow: 0px 0px 2px #AAAAAA */ +} \ No newline at end of file diff --git a/Docs/Waitting.md b/Docs/Waitting.md new file mode 100644 index 00000000..e932d56d --- /dev/null +++ b/Docs/Waitting.md @@ -0,0 +1 @@ +Waiting~~~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7a433bf7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Italink + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Resources/Audio/I Say Yeah.m4a b/Resources/Audio/I Say Yeah.m4a new file mode 100644 index 00000000..4b297b12 Binary files /dev/null and b/Resources/Audio/I Say Yeah.m4a differ diff --git a/Source/Resources/Audio/MySunset.mp3 b/Resources/Audio/MySunset.mp3 similarity index 100% rename from Source/Resources/Audio/MySunset.mp3 rename to Resources/Audio/MySunset.mp3 diff --git a/Resources/Image/Grid.png b/Resources/Image/Grid.png new file mode 100644 index 00000000..2932338f Binary files /dev/null and b/Resources/Image/Grid.png differ diff --git a/Source/Resources/Image/Logo.png b/Resources/Image/Logo.png similarity index 100% rename from Source/Resources/Image/Logo.png rename to Resources/Image/Logo.png diff --git a/Source/Resources/Image/Skybox.jpeg b/Resources/Image/Skybox.jpeg similarity index 100% rename from Source/Resources/Image/Skybox.jpeg rename to Resources/Image/Skybox.jpeg diff --git a/Source/Resources/Image/environment.hdr b/Resources/Image/environment.hdr similarity index 100% rename from Source/Resources/Image/environment.hdr rename to Resources/Image/environment.hdr diff --git a/Source/Resources/Model/Catwalk Walk Turn 180 Tight R.fbx b/Resources/Model/Catwalk Walk Turn 180 Tight R.fbx similarity index 100% rename from Source/Resources/Model/Catwalk Walk Turn 180 Tight R.fbx rename to Resources/Model/Catwalk Walk Turn 180 Tight R.fbx diff --git a/Source/Resources/Model/mandalorian/license.txt b/Resources/Model/mandalorian/license.txt similarity index 100% rename from Source/Resources/Model/mandalorian/license.txt rename to Resources/Model/mandalorian/license.txt diff --git a/Source/Resources/Model/mandalorian/scene.bin b/Resources/Model/mandalorian/scene.bin similarity index 100% rename from Source/Resources/Model/mandalorian/scene.bin rename to Resources/Model/mandalorian/scene.bin diff --git a/Source/Resources/Model/mandalorian/scene.gltf b/Resources/Model/mandalorian/scene.gltf similarity index 100% rename from Source/Resources/Model/mandalorian/scene.gltf rename to Resources/Model/mandalorian/scene.gltf diff --git a/Source/Resources/Model/mandalorian/textures/Armor_Plate_Comb_baseColor.png b/Resources/Model/mandalorian/textures/Armor_Plate_Comb_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Armor_Plate_Comb_baseColor.png rename to Resources/Model/mandalorian/textures/Armor_Plate_Comb_baseColor.png diff --git a/Source/Resources/Model/mandalorian/textures/Armor_Plate_Comb_metallicRoughness.png b/Resources/Model/mandalorian/textures/Armor_Plate_Comb_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Armor_Plate_Comb_metallicRoughness.png rename to Resources/Model/mandalorian/textures/Armor_Plate_Comb_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian/textures/Armor_Plate_Comb_normal.png b/Resources/Model/mandalorian/textures/Armor_Plate_Comb_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Armor_Plate_Comb_normal.png rename to Resources/Model/mandalorian/textures/Armor_Plate_Comb_normal.png diff --git a/Source/Resources/Model/mandalorian/textures/Beskar_Golden_normal.png b/Resources/Model/mandalorian/textures/Beskar_Golden_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Beskar_Golden_normal.png rename to Resources/Model/mandalorian/textures/Beskar_Golden_normal.png diff --git a/Source/Resources/Model/mandalorian/textures/Beskar_baseColor.png b/Resources/Model/mandalorian/textures/Beskar_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Beskar_baseColor.png rename to Resources/Model/mandalorian/textures/Beskar_baseColor.png diff --git a/Source/Resources/Model/mandalorian/textures/Beskar_metallicRoughness.png b/Resources/Model/mandalorian/textures/Beskar_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Beskar_metallicRoughness.png rename to Resources/Model/mandalorian/textures/Beskar_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian/textures/Beskar_normal.png b/Resources/Model/mandalorian/textures/Beskar_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Beskar_normal.png rename to Resources/Model/mandalorian/textures/Beskar_normal.png diff --git a/Source/Resources/Model/mandalorian/textures/Blaster_combined_baseColor.png b/Resources/Model/mandalorian/textures/Blaster_combined_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Blaster_combined_baseColor.png rename to Resources/Model/mandalorian/textures/Blaster_combined_baseColor.png diff --git a/Source/Resources/Model/mandalorian/textures/Blaster_combined_metallicRoughness.png b/Resources/Model/mandalorian/textures/Blaster_combined_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Blaster_combined_metallicRoughness.png rename to Resources/Model/mandalorian/textures/Blaster_combined_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian/textures/Blaster_combined_normal.png b/Resources/Model/mandalorian/textures/Blaster_combined_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Blaster_combined_normal.png rename to Resources/Model/mandalorian/textures/Blaster_combined_normal.png diff --git a/Source/Resources/Model/mandalorian/textures/Cape_Material_baseColor.png b/Resources/Model/mandalorian/textures/Cape_Material_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Cape_Material_baseColor.png rename to Resources/Model/mandalorian/textures/Cape_Material_baseColor.png diff --git a/Source/Resources/Model/mandalorian/textures/Cape_Material_metallicRoughness.png b/Resources/Model/mandalorian/textures/Cape_Material_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Cape_Material_metallicRoughness.png rename to Resources/Model/mandalorian/textures/Cape_Material_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian/textures/Cape_Material_normal.png b/Resources/Model/mandalorian/textures/Cape_Material_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Cape_Material_normal.png rename to Resources/Model/mandalorian/textures/Cape_Material_normal.png diff --git a/Source/Resources/Model/mandalorian/textures/Floor_Color_baseColor.png b/Resources/Model/mandalorian/textures/Floor_Color_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Floor_Color_baseColor.png rename to Resources/Model/mandalorian/textures/Floor_Color_baseColor.png diff --git a/Source/Resources/Model/mandalorian/textures/Floor_Color_metallicRoughness.png b/Resources/Model/mandalorian/textures/Floor_Color_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Floor_Color_metallicRoughness.png rename to Resources/Model/mandalorian/textures/Floor_Color_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian/textures/Floor_Color_normal.png b/Resources/Model/mandalorian/textures/Floor_Color_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Floor_Color_normal.png rename to Resources/Model/mandalorian/textures/Floor_Color_normal.png diff --git a/Source/Resources/Model/mandalorian/textures/Full_Rifle_baseColor.png b/Resources/Model/mandalorian/textures/Full_Rifle_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Full_Rifle_baseColor.png rename to Resources/Model/mandalorian/textures/Full_Rifle_baseColor.png diff --git a/Source/Resources/Model/mandalorian/textures/Full_Rifle_metallicRoughness.png b/Resources/Model/mandalorian/textures/Full_Rifle_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Full_Rifle_metallicRoughness.png rename to Resources/Model/mandalorian/textures/Full_Rifle_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian/textures/Full_Rifle_normal.png b/Resources/Model/mandalorian/textures/Full_Rifle_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Full_Rifle_normal.png rename to Resources/Model/mandalorian/textures/Full_Rifle_normal.png diff --git a/Source/Resources/Model/mandalorian/textures/Grogu_Combined_baseColor.png b/Resources/Model/mandalorian/textures/Grogu_Combined_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Grogu_Combined_baseColor.png rename to Resources/Model/mandalorian/textures/Grogu_Combined_baseColor.png diff --git a/Source/Resources/Model/mandalorian/textures/Grogu_Combined_metallicRoughness.png b/Resources/Model/mandalorian/textures/Grogu_Combined_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Grogu_Combined_metallicRoughness.png rename to Resources/Model/mandalorian/textures/Grogu_Combined_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian/textures/Grogu_Combined_normal.png b/Resources/Model/mandalorian/textures/Grogu_Combined_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Grogu_Combined_normal.png rename to Resources/Model/mandalorian/textures/Grogu_Combined_normal.png diff --git a/Source/Resources/Model/mandalorian/textures/Inner_Armor_Combined_baseColor.png b/Resources/Model/mandalorian/textures/Inner_Armor_Combined_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Inner_Armor_Combined_baseColor.png rename to Resources/Model/mandalorian/textures/Inner_Armor_Combined_baseColor.png diff --git a/Source/Resources/Model/mandalorian/textures/Inner_Armor_Combined_metallicRoughness.png b/Resources/Model/mandalorian/textures/Inner_Armor_Combined_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Inner_Armor_Combined_metallicRoughness.png rename to Resources/Model/mandalorian/textures/Inner_Armor_Combined_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian/textures/Inner_Armor_Combined_normal.png b/Resources/Model/mandalorian/textures/Inner_Armor_Combined_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian/textures/Inner_Armor_Combined_normal.png rename to Resources/Model/mandalorian/textures/Inner_Armor_Combined_normal.png diff --git a/Source/Resources/Model/mandalorian_grogu/license.txt b/Resources/Model/mandalorian_grogu/license.txt similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/license.txt rename to Resources/Model/mandalorian_grogu/license.txt diff --git a/Source/Resources/Model/mandalorian_grogu/scene.bin b/Resources/Model/mandalorian_grogu/scene.bin similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/scene.bin rename to Resources/Model/mandalorian_grogu/scene.bin diff --git a/Source/Resources/Model/mandalorian_grogu/scene.gltf b/Resources/Model/mandalorian_grogu/scene.gltf similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/scene.gltf rename to Resources/Model/mandalorian_grogu/scene.gltf diff --git a/Source/Resources/Model/mandalorian_grogu/textures/Backpack_Carrier_Cosmos_baseColor.png b/Resources/Model/mandalorian_grogu/textures/Backpack_Carrier_Cosmos_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/Backpack_Carrier_Cosmos_baseColor.png rename to Resources/Model/mandalorian_grogu/textures/Backpack_Carrier_Cosmos_baseColor.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/Backpack_Carrier_Cosmos_metallicRoughness.png b/Resources/Model/mandalorian_grogu/textures/Backpack_Carrier_Cosmos_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/Backpack_Carrier_Cosmos_metallicRoughness.png rename to Resources/Model/mandalorian_grogu/textures/Backpack_Carrier_Cosmos_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/MI_Cosmos_JetPack_baseColor.png b/Resources/Model/mandalorian_grogu/textures/MI_Cosmos_JetPack_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/MI_Cosmos_JetPack_baseColor.png rename to Resources/Model/mandalorian_grogu/textures/MI_Cosmos_JetPack_baseColor.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/MI_Cosmos_JetPack_metallicRoughness.png b/Resources/Model/mandalorian_grogu/textures/MI_Cosmos_JetPack_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/MI_Cosmos_JetPack_metallicRoughness.png rename to Resources/Model/mandalorian_grogu/textures/MI_Cosmos_JetPack_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/MI_Rifle_Cosmo_baseColor.png b/Resources/Model/mandalorian_grogu/textures/MI_Rifle_Cosmo_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/MI_Rifle_Cosmo_baseColor.png rename to Resources/Model/mandalorian_grogu/textures/MI_Rifle_Cosmo_baseColor.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/MI_Rifle_Cosmo_metallicRoughness.png b/Resources/Model/mandalorian_grogu/textures/MI_Rifle_Cosmo_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/MI_Rifle_Cosmo_metallicRoughness.png rename to Resources/Model/mandalorian_grogu/textures/MI_Rifle_Cosmo_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Armor_baseColor.png b/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Armor_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Armor_baseColor.png rename to Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Armor_baseColor.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Armor_metallicRoughness.png b/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Armor_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Armor_metallicRoughness.png rename to Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Armor_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Body_baseColor.png b/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Body_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Body_baseColor.png rename to Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Body_baseColor.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Body_metallicRoughness.png b/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Body_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Body_metallicRoughness.png rename to Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Body_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_baseColor.png b/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_baseColor.png rename to Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_baseColor.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_metallicRoughness.png b/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_metallicRoughness.png rename to Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_normal.png b/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_normal.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_normal.png rename to Resources/Model/mandalorian_grogu/textures/M_MED_Cosmos_Head_normal.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/Pet_Cosmos_baseColor.png b/Resources/Model/mandalorian_grogu/textures/Pet_Cosmos_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/Pet_Cosmos_baseColor.png rename to Resources/Model/mandalorian_grogu/textures/Pet_Cosmos_baseColor.png diff --git a/Source/Resources/Model/mandalorian_grogu/textures/Pet_Cosmos_metallicRoughness.png b/Resources/Model/mandalorian_grogu/textures/Pet_Cosmos_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_grogu/textures/Pet_Cosmos_metallicRoughness.png rename to Resources/Model/mandalorian_grogu/textures/Pet_Cosmos_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_ship/license.txt b/Resources/Model/mandalorian_ship/license.txt similarity index 100% rename from Source/Resources/Model/mandalorian_ship/license.txt rename to Resources/Model/mandalorian_ship/license.txt diff --git a/Source/Resources/Model/mandalorian_ship/scene.bin b/Resources/Model/mandalorian_ship/scene.bin similarity index 100% rename from Source/Resources/Model/mandalorian_ship/scene.bin rename to Resources/Model/mandalorian_ship/scene.bin diff --git a/Source/Resources/Model/mandalorian_ship/scene.gltf b/Resources/Model/mandalorian_ship/scene.gltf similarity index 100% rename from Source/Resources/Model/mandalorian_ship/scene.gltf rename to Resources/Model/mandalorian_ship/scene.gltf diff --git a/Source/Resources/Model/mandalorian_ship/textures/Armas_baseColor.png b/Resources/Model/mandalorian_ship/textures/Armas_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Armas_baseColor.png rename to Resources/Model/mandalorian_ship/textures/Armas_baseColor.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/Armas_metallicRoughness.png b/Resources/Model/mandalorian_ship/textures/Armas_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Armas_metallicRoughness.png rename to Resources/Model/mandalorian_ship/textures/Armas_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/Armas_specularf0.png b/Resources/Model/mandalorian_ship/textures/Armas_specularf0.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Armas_specularf0.png rename to Resources/Model/mandalorian_ship/textures/Armas_specularf0.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/Carroceria_baseColor.png b/Resources/Model/mandalorian_ship/textures/Carroceria_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Carroceria_baseColor.png rename to Resources/Model/mandalorian_ship/textures/Carroceria_baseColor.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/Carroceria_metallicRoughness.png b/Resources/Model/mandalorian_ship/textures/Carroceria_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Carroceria_metallicRoughness.png rename to Resources/Model/mandalorian_ship/textures/Carroceria_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/Carroceria_specularf0.png b/Resources/Model/mandalorian_ship/textures/Carroceria_specularf0.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Carroceria_specularf0.png rename to Resources/Model/mandalorian_ship/textures/Carroceria_specularf0.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/MotorT_baseColor.png b/Resources/Model/mandalorian_ship/textures/MotorT_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/MotorT_baseColor.png rename to Resources/Model/mandalorian_ship/textures/MotorT_baseColor.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/MotorT_metallicRoughness.png b/Resources/Model/mandalorian_ship/textures/MotorT_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/MotorT_metallicRoughness.png rename to Resources/Model/mandalorian_ship/textures/MotorT_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/MotorT_specularf0.png b/Resources/Model/mandalorian_ship/textures/MotorT_specularf0.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/MotorT_specularf0.png rename to Resources/Model/mandalorian_ship/textures/MotorT_specularf0.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/Turbinas_baseColor.png b/Resources/Model/mandalorian_ship/textures/Turbinas_baseColor.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Turbinas_baseColor.png rename to Resources/Model/mandalorian_ship/textures/Turbinas_baseColor.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/Turbinas_metallicRoughness.png b/Resources/Model/mandalorian_ship/textures/Turbinas_metallicRoughness.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Turbinas_metallicRoughness.png rename to Resources/Model/mandalorian_ship/textures/Turbinas_metallicRoughness.png diff --git a/Source/Resources/Model/mandalorian_ship/textures/Turbinas_specularf0.png b/Resources/Model/mandalorian_ship/textures/Turbinas_specularf0.png similarity index 100% rename from Source/Resources/Model/mandalorian_ship/textures/Turbinas_specularf0.png rename to Resources/Model/mandalorian_ship/textures/Turbinas_specularf0.png diff --git a/Source/1-GraphicsAPI/04-Buffer/color.frag b/Resources/Shader/color.frag similarity index 100% rename from Source/1-GraphicsAPI/04-Buffer/color.frag rename to Resources/Shader/color.frag diff --git a/Resources/Shader/color.frag.qsb b/Resources/Shader/color.frag.qsb new file mode 100644 index 00000000..0c971837 Binary files /dev/null and b/Resources/Shader/color.frag.qsb differ diff --git a/Source/Resources/Video/BadApple.mp4 b/Resources/Video/BadApple.mp4 similarity index 100% rename from Source/Resources/Video/BadApple.mp4 rename to Resources/Video/BadApple.mp4 diff --git a/Source/0-QEngineUtilities b/Source/0-QEngineUtilities index 2d608e73..cbb51567 160000 --- a/Source/0-QEngineUtilities +++ b/Source/0-QEngineUtilities @@ -1 +1 @@ -Subproject commit 2d608e7362b36a01b4f5b8227f50873b423496ee +Subproject commit cbb51567c5bea0a8f870618851e2e69f1b26657e diff --git a/Source/1-GraphicsAPI/01-WindowAndWidget/Source/main.cpp b/Source/1-GraphicsAPI/01-WindowAndWidget/Source/main.cpp index e58695e2..ee2d1db5 100644 --- a/Source/1-GraphicsAPI/01-WindowAndWidget/Source/main.cpp +++ b/Source/1-GraphicsAPI/01-WindowAndWidget/Source/main.cpp @@ -55,26 +55,26 @@ class ExampleRhiWidget : public QRhiWidget { class ExampleRhiWindow : public QRhiWindow { public: - ExampleRhiWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) {} + ExampleRhiWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) {} protected: virtual void onRenderTick() override { QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 1.0f, 1.0f); const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - currentCmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue); - currentCmdBuffer->endPass(); + cmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue); + cmdBuffer->endPass(); } }; int main(int argc, char **argv) { - qputenv("QSG_INFO", "1"); QApplication app(argc, argv); - QRhiWindow::InitParams initParams; + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; ExampleRhiWindow window(initParams); window.setTitle("01-RhiWindow"); window.resize({ 400,400 }); diff --git a/Source/1-GraphicsAPI/02-GraphicRenderingPipeline/Source/main.cpp b/Source/1-GraphicsAPI/02-GraphicRenderingPipeline/Source/main.cpp index 7c20c899..87308220 100644 --- a/Source/1-GraphicsAPI/02-GraphicRenderingPipeline/Source/main.cpp +++ b/Source/1-GraphicsAPI/02-GraphicRenderingPipeline/Source/main.cpp @@ -1,7 +1,8 @@ #include +#include "QEngineApplication.h" #include "Render/RHI/QRhiWindow.h" -static float VertexData[] = { +static float VertexData[] = { //顶点数据 //position(xy) color(rgba) 0.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, @@ -10,119 +11,105 @@ static float VertexData[] = { class TriangleWindow : public QRhiWindow { private: - QRhiEx::Signal sigInit; - QRhiEx::Signal sigSubmit; - QScopedPointer mVertexBuffer; - QScopedPointer mShaderBindings; - QScopedPointer mPipeline; + QRhiSignal mSigInit; //用于初始化的信号 + QRhiSignal mSigSubmit; //用于提交资源的信号 + QScopedPointer mVertexBuffer; //顶点缓冲区 + QScopedPointer mShaderBindings; //描述符集布局绑定 + QScopedPointer mPipeline; //图形渲染管线 public: - TriangleWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) { - sigInit.request(); - sigSubmit.request(); + TriangleWindow(QRhiHelper::InitParams inInitParams) + : QRhiWindow(inInitParams) { + mSigInit.request(); //请求初始化 + mSigSubmit.request(); //请求提交资源 } protected: - void initRhiResource() { - mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); - mVertexBuffer->create(); - - mShaderBindings.reset(mRhi->newShaderResourceBindings()); //创建绑定 - mShaderBindings->create(); - - mPipeline.reset(mRhi->newGraphicsPipeline()); - - mPipeline->setShaderResourceBindings(mShaderBindings.get()); //绑定到流水线中华 - - QRhiGraphicsPipeline::TargetBlend targetBlend; - targetBlend.enable = false; - mPipeline->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); - - mPipeline->setSampleCount(mSwapChain->sampleCount()); - - mPipeline->setDepthTest(false); - mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); - mPipeline->setDepthWrite(false); - - - QRhiVertexInputLayout inputLayout; - inputLayout.setBindings({ - QRhiVertexInputBinding(6 * sizeof(float)) //定义每个VertexBuffer,单组顶点数据的跨度,这里是 6 * sizeof(float),可以当作是GPU会从Buffer0(0是Index)读取 6 * sizeof(float) 传给 Vertex Shader - }); - - inputLayout.setAttributes({ - QRhiVertexInputAttribute(0, 0 , QRhiVertexInputAttribute::Float2, 0), // 从每组顶点数据的位置 0 开始作为 Location0(Float2) 的起始地址 - QRhiVertexInputAttribute(0, 1 , QRhiVertexInputAttribute::Float4, sizeof(float) * 2), // 从每组顶点数据的位置 sizeof(float) * 2 开始作为 Location1(Float4) 的起始地址 - }); - - mPipeline->setVertexInputLayout(inputLayout); - - QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 - layout(location = 0) in vec2 position; //这里需要与上面的inputLayout 对应 - layout(location = 1) in vec4 color; - - layout (location = 0) out vec4 vColor; //输出变量到 fragment shader 中,这里的location是out的,而不是in - - out gl_PerVertex { - vec4 gl_Position; - }; - - void main(){ - gl_Position = vec4(position,0.0f,1.0f); - vColor = color; - } - )"); - Q_ASSERT(vs.isValid()); - - QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 - layout (location = 0) in vec4 vColor; //上一阶段的out变成了这一阶段的in - layout (location = 0) out vec4 fragColor; - void main(){ - fragColor = vColor; - } - )"); - Q_ASSERT(fs.isValid()); - - mPipeline->setShaderStages({ - QRhiShaderStage(QRhiShaderStage::Vertex, vs), - QRhiShaderStage(QRhiShaderStage::Fragment, fs) - }); - mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); - mPipeline->create(); - } - virtual void onRenderTick() override { - QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - if (sigInit.receive()) { - initRhiResource(); - } - QRhiResourceUpdateBatch* resourceUpdates = nullptr; - if (sigSubmit.receive()) { - resourceUpdates = mRhi->nextResourceUpdateBatch(); - resourceUpdates->uploadStaticBuffer(mVertexBuffer.get(), VertexData); //上传顶点数据 + if (mSigInit.ensure()) { //初始化资源 + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + mVertexBuffer->create(); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + QRhiVertexInputBinding(6 * sizeof(float)) //定义每个VertexBuffer,单组顶点数据的跨度,这里是 6 * sizeof(float),可以当作是GPU会从Buffer0(0是Index)读取 6 * sizeof(float) 传给 Vertex Shader + }); + + inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0 , QRhiVertexInputAttribute::Float2, 0), // 从每组顶点数据的位置 0 开始作为 Location0(Float2) 的起始地址 + QRhiVertexInputAttribute(0, 1 , QRhiVertexInputAttribute::Float4, sizeof(float) * 2), // 从每组顶点数据的位置 sizeof(float) * 2 开始作为 Location1(Float4) 的起始地址 + }); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + + mPipeline->setVertexInputLayout(inputLayout); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); //创建绑定 + mShaderBindings->create(); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); //绑定到流水线 + mPipeline->setSampleCount(mSwapChain->sampleCount()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; //这里需要与上面的inputLayout 对应 + layout(location = 1) in vec4 color; + + layout (location = 0) out vec4 vColor; //输出变量到 fragment shader 中,这里的location是out的,而不是in + + out gl_PerVertex { + vec4 gl_Position; + }; + + void main(){ + gl_Position = vec4(position,0.0f,1.0f); + vColor = color; + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout (location = 0) in vec4 vColor; //上一阶段的out变成了这一阶段的in + layout (location = 0) out vec4 fragColor; //片段着色器输入 + void main(){ + fragColor = vColor; + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + QRhiShaderStage(QRhiShaderStage::Vertex, vs), + QRhiShaderStage(QRhiShaderStage::Fragment, fs) + }); + + mPipeline->create(); } - const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); //交互链中的当前渲染目标 + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); //交互链中的当前指令缓冲 + + if (mSigSubmit.ensure()) { + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); //申请资源操作合批入口 + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); //上传顶点数据 + cmdBuffer->resourceUpdate(batch); + } - currentCmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue, resourceUpdates); + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); //使用该值来清理 渲染目标 中的 颜色附件 + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; //使用该值来清理 渲染目标 中的 深度和模板附件 + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, nullptr); //开启渲染通道 - currentCmdBuffer->setGraphicsPipeline(mPipeline.get()); - currentCmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); - currentCmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); //将 mVertexBuffer 绑定到Buffer0,内存偏移值为0 - currentCmdBuffer->setVertexInput(0, 1, &vertexBindings); - currentCmdBuffer->draw(3); + cmdBuffer->setGraphicsPipeline(mPipeline.get()); //设置图形渲染管线 + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); //设置图像的绘制区域 + cmdBuffer->setShaderResources(); //设置描述符集布局绑定,如果不填参数(为nullptr),则会使用渲染管线创建时所使用的描述符集布局绑定 + const QRhiCommandBuffer::VertexInput vertexInput(mVertexBuffer.get(), 0); //将 mVertexBuffer 绑定到Buffer0,内存偏移值为0 + cmdBuffer->setVertexInput(0, 1, &vertexInput); + cmdBuffer->draw(3); //执行绘制,其中 3 代表着有 3个顶点数据 输入 - currentCmdBuffer->endPass(); + cmdBuffer->endPass(); //结束渲染通道 } }; int main(int argc, char **argv) { - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; + QEngineApplication app(argc, argv); + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; TriangleWindow window(initParams); window.resize({ 800,600 }); window.show(); diff --git a/Source/1-GraphicsAPI/03-Shader/Source/main.cpp b/Source/1-GraphicsAPI/03-Shader/Source/main.cpp index 31040785..69ec66a5 100644 --- a/Source/1-GraphicsAPI/03-Shader/Source/main.cpp +++ b/Source/1-GraphicsAPI/03-Shader/Source/main.cpp @@ -2,27 +2,28 @@ #include #include #include -#include "private/qshaderbaker_p.h" #include "QWidget" #include "QTextEdit" #include "QTextBrowser" #include "QBoxLayout" #include "QPushButton" #include "QLabel" +#include "rhi/qshader.h" +#include "rhi/qshaderbaker.h" -QShader createShaderFromCode(QShader::Stage stage, const char* code) { - QShaderBaker baker; +QShader newShaderFromCode(QShader::Stage stage, const char* code) { + QShaderBaker baker; //着色器烘培器 baker.setGeneratedShaderVariants({ QShader::StandardShader }); baker.setGeneratedShaders({ - QShaderBaker::GeneratedShader{ QShader::Source::SpirvShader,QShaderVersion(100)}, + QShaderBaker::GeneratedShader{QShader::Source::SpirvShader,QShaderVersion(100)}, QShaderBaker::GeneratedShader{QShader::Source::GlslShader,QShaderVersion(430)}, QShaderBaker::GeneratedShader{QShader::Source::MslShader,QShaderVersion(12)}, QShaderBaker::GeneratedShader{QShader::Source::HlslShader,QShaderVersion(60)}, }); + baker.setSourceString(code, stage); //装配GLSL源码 + QShader shader = baker.bake(); //执行烘培,将之编译成各个图形API的着色器代码 - baker.setSourceString(code, stage); - QShader shader = baker.bake(); - if (!shader.isValid()) { + if (!shader.isValid()) { //打印编译报错 QStringList codelist = QString(code).split('\n'); for (int i = 0; i < codelist.size(); i++) { qWarning() << i + 1 << codelist[i].toLocal8Bit().data(); @@ -32,10 +33,10 @@ QShader createShaderFromCode(QShader::Stage stage, const char* code) { return shader; } -QShader createShaderFromFile(QShader::Stage stage, const char* filename) { +QShader newShaderFromFile(QShader::Stage stage, const char* filename) { QFile file(filename); if (file.open(QIODevice::ReadOnly)) - return createShaderFromCode(stage,file.readAll().constData()); + return newShaderFromCode(stage,file.readAll().constData()); return QShader(); } @@ -46,11 +47,10 @@ QShader newShaderFromQSBFile(const char* filename) { return QShader(); } -void RunQtShaderTool() { +void RunQtShaderToolByQProcess() { QProcess runShaderTool; runShaderTool.setProgram("qsb"); runShaderTool.setProcessChannelMode(QProcess::MergedChannels); - runShaderTool.setArguments({ "--glsl","430", "--msl","12" , @@ -66,13 +66,13 @@ void RunQtShaderTool() { int main(int argc, char **argv) { QApplication app(argc, argv); - QDir::setCurrent(RESOURCE_DIR"/Shader"); + QDir::setCurrent("Resources/Shader"); QWidget main; QGridLayout* layout = new QGridLayout(&main); - QPushButton* btCompile = new QPushButton("Compile"); + QPushButton* btCompile = new QPushButton("Vulkan GLSL"); QTextEdit* editor = new QTextEdit; QTextBrowser* glslBrowser = new QTextBrowser; QTextBrowser* hlslBrowser = new QTextBrowser; @@ -81,7 +81,7 @@ int main(int argc, char **argv) layout->addWidget(btCompile, 0, 0); layout->addWidget(editor, 1, 0); - layout->addWidget(new QLabel("GLSL"), 0, 1, Qt::AlignCenter); + layout->addWidget(new QLabel("OpenGL GLSL"), 0, 1, Qt::AlignCenter); layout->addWidget(glslBrowser, 1, 1); layout->addWidget(new QLabel("HLSL"), 0, 2, Qt::AlignCenter); @@ -103,7 +103,7 @@ void main() })"); QObject::connect(btCompile, &QPushButton::clicked, [&]() { - QShader shader = createShaderFromCode(QShader::FragmentStage, editor->toPlainText().toLocal8Bit()); + QShader shader = newShaderFromCode(QShader::FragmentStage, editor->toPlainText().toLocal8Bit()); glslBrowser->setText(shader.shader(QShaderKey(QShader::GlslShader, QShaderVersion(430))).shader()); hlslBrowser->setText(shader.shader(QShaderKey(QShader::HlslShader, QShaderVersion(60))).shader()); mslBrowser->setText(shader.shader(QShaderKey(QShader::MslShader, QShaderVersion(12))).shader()); diff --git a/Source/1-GraphicsAPI/04-Buffer/Source/main.cpp b/Source/1-GraphicsAPI/04-Buffer/Source/main.cpp deleted file mode 100644 index 4fa1ffff..00000000 --- a/Source/1-GraphicsAPI/04-Buffer/Source/main.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include -#include -#include -#include -#include "private/qshaderbaker_p.h" - -QShader newShaderFromCode(QShader::Stage stage, const char* code) { - QShaderBaker baker; - baker.setGeneratedShaderVariants({ QShader::StandardShader }); - baker.setGeneratedShaders({ - QShaderBaker::GeneratedShader{QShader::Source::SpirvShader,QShaderVersion(100)}, - QShaderBaker::GeneratedShader{QShader::Source::GlslShader,QShaderVersion(430)}, - QShaderBaker::GeneratedShader{QShader::Source::MslShader,QShaderVersion(12)}, - QShaderBaker::GeneratedShader{QShader::Source::HlslShader,QShaderVersion(60)}, - }); - - baker.setSourceString(code, stage); - QShader shader = baker.bake(); - if (!shader.isValid()) { - qWarning(code); - qWarning(baker.errorMessage().toLocal8Bit()); - } - return shader; -} - -QShader newShaderFromFile(QShader::Stage stage, const char* filename) { - QFile file(filename); - if (file.open(QIODevice::ReadOnly)) - return newShaderFromCode(stage,file.readAll().constData()); - return QShader(); -} - -QShader newShaderFromQSBFile(const char* filename) { - QFile file(filename); - if (file.open(QIODevice::ReadOnly)) - return QShader::fromSerialized(file.readAll()); - return QShader(); -} - -void RunQtShaderTool() { - QProcess runShaderTool; - runShaderTool.setProgram("qsb"); - runShaderTool.setProcessChannelMode(QProcess::MergedChannels); - - runShaderTool.setArguments({ - "--glsl","430", - "--msl","12" , - "-c", "color.frag", - "-o","color.frag.qsb" }); - runShaderTool.start(); - runShaderTool.waitForFinished(); - qDebug() << runShaderTool.readAllStandardOutput().constData(); - - runShaderTool.setArguments({ - "--glsl","430", - "--msl","12" , - "-c", "color.vert", - "-o","color.vert.qsb" }); - - runShaderTool.start(); - runShaderTool.waitForFinished(); - - qDebug() << runShaderTool.readAllStandardOutput().constData(); -} - -int main(int argc, char **argv) -{ - QApplication app(argc, argv); - QDir::setCurrent(RESOURCE_DIR); - RunQtShaderTool(); - return app.exec(); -} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/04-Buffer/color.frag.qsb b/Source/1-GraphicsAPI/04-Buffer/color.frag.qsb deleted file mode 100644 index 3c9de606..00000000 Binary files a/Source/1-GraphicsAPI/04-Buffer/color.frag.qsb and /dev/null differ diff --git a/Source/1-GraphicsAPI/04-Buffer/color.vert b/Source/1-GraphicsAPI/04-Buffer/color.vert deleted file mode 100644 index 43af543f..00000000 --- a/Source/1-GraphicsAPI/04-Buffer/color.vert +++ /dev/null @@ -1,19 +0,0 @@ -#version 440 - -layout(location = 0) in vec4 position; -layout(location = 1) in vec3 color; - -layout(location = 0) out vec3 v_color; - -layout(std140, binding = 0) uniform buf { - mat4 mvp; - float opacity; -} ubuf; - -out gl_PerVertex { vec4 gl_Position; }; - -void main() -{ - v_color = color; - gl_Position = ubuf.mvp * position; -} diff --git a/Source/1-GraphicsAPI/04-Buffer/color.vert.qsb b/Source/1-GraphicsAPI/04-Buffer/color.vert.qsb deleted file mode 100644 index 716bd4d2..00000000 Binary files a/Source/1-GraphicsAPI/04-Buffer/color.vert.qsb and /dev/null differ diff --git a/Source/1-GraphicsAPI/04-Buffer/README.md b/Source/1-GraphicsAPI/04-BufferAndTexture/README.md similarity index 100% rename from Source/1-GraphicsAPI/04-Buffer/README.md rename to Source/1-GraphicsAPI/04-BufferAndTexture/README.md diff --git a/Source/1-GraphicsAPI/04-BufferAndTexture/Source/main.cpp b/Source/1-GraphicsAPI/04-BufferAndTexture/Source/main.cpp new file mode 100644 index 00000000..c58e9089 --- /dev/null +++ b/Source/1-GraphicsAPI/04-BufferAndTexture/Source/main.cpp @@ -0,0 +1,192 @@ +#include +#include "Render/RHI/QRhiWindow.h" +#include "QDateTime" + +static float VertexData[] = { + //position(xy) texture coord(uv) + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + -1.0f, -1.0f, 0.0f, 0.0f, + -1.0f, 1.0f, 0.0f, 1.0f, +}; + +static uint32_t IndexData[] = { + 0,1,2, + 2,3,0 +}; + +struct UniformBlock { + float time; + alignas(8) QVector2D mousePos; +}; + +class MyWindow : public QRhiWindow { +public: + MyWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + } +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QImage mImage; + QScopedPointer mVertexBuffer; + QScopedPointer mIndexBuffer; + QScopedPointer mUniformBuffer; + QScopedPointer mTexture; + QScopedPointer mSampler; + QScopedPointer mShaderBindings; + QScopedPointer mPipeline; +protected: + void initRhiResource() { + mImage = QImage("Resources/Image/Logo.png").convertedTo(QImage::Format_RGBA8888); + + mTexture.reset(mRhi->newTexture(QRhiTexture::RGBA8, mImage.size())); + mTexture->create(); + + mSampler.reset(mRhi->newSampler( + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::Nearest, + QRhiSampler::Filter::Linear, + QRhiSampler::AddressMode::Repeat, + QRhiSampler::AddressMode::Repeat, + QRhiSampler::AddressMode::Repeat + )); + mSampler->create(); + + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + mVertexBuffer->create(); + + mIndexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndexData))); + mIndexBuffer->create(); + + mUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); + mUniformBuffer->create(); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + + mShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::StageFlag::FragmentStage, mUniformBuffer.get()), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::StageFlag::FragmentStage, mTexture.get(),mSampler.get()) + }); + + mShaderBindings->create(); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + + QRhiGraphicsPipeline::TargetBlend targetBlend; + targetBlend.enable = false; + mPipeline->setTargetBlends({ targetBlend }); + + mPipeline->setSampleCount(mSwapChain->sampleCount()); + mPipeline->setTopology(QRhiGraphicsPipeline::Triangles); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 inPosition; + layout(location = 1) in vec2 inUV; + + layout(location = 0) out vec2 vUV; + + out gl_PerVertex { vec4 gl_Position; }; + + void main() + { + vUV = inUV; + gl_Position = vec4(inPosition,0.0f,1.0f); + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(location = 0) in vec2 vUV; + layout(location = 0) out vec4 outFragColor; + + layout(binding = 0) uniform UniformBlock{ + float time; + vec2 mousePos; + }UBO; + + layout(binding = 1) uniform sampler2D inTexture; + + void main() + { + vec4 textureColor = texture(inTexture,vUV); //当前片段的纹理颜色 + + const float speed = 5.0f; + vec4 mixColor = vec4(1.0f) * sin(UBO.time * speed); //根据时间因子和正弦函数,制作一个随时间发生明暗变化的颜色,你可以使用 outFragColor = mixColor 查看它的输出 + + outFragColor = mix(textureColor, mixColor, distance(gl_FragCoord.xy,UBO.mousePos)/500); //根据当时鼠标位置跟像素坐标的距离,来混合两种颜色 + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 4 * sizeof(float) } + }); + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, + { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) } + }); + + mPipeline->setVertexInputLayout(inputLayout); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mPipeline->create(); + + mSigSubmit.request(); + } + + virtual void onRenderTick() override { + if (mSigInit.ensure()) { + initRhiResource(); + } + + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + if (mSigSubmit.ensure()) { + batch->uploadStaticBuffer(mIndexBuffer.get(), IndexData); + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + batch->uploadTexture(mTexture.get(), mImage); + } + UniformBlock ubo; + ubo.mousePos = QVector2D(mapFromGlobal(QCursor::pos())) * qApp->devicePixelRatio(); + ubo.time = QTime::currentTime().msecsSinceStartOfDay() / 1000.0f; + batch->updateDynamicBuffer(mUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); + + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, batch); + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexBindings, mIndexBuffer.get(), 0, QRhiCommandBuffer::IndexUInt32); + cmdBuffer->drawIndexed(6); + + cmdBuffer->endPass(); + } +}; + +int main(int argc, char **argv){ + QApplication app(argc, argv); + + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + MyWindow* window = new MyWindow(initParams); + window->resize({ 800,600 }); + window->show(); + + app.exec(); + delete window; + return 0; +} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/05-Texture/README.md b/Source/1-GraphicsAPI/05-3D/README.md similarity index 100% rename from Source/1-GraphicsAPI/05-Texture/README.md rename to Source/1-GraphicsAPI/05-3D/README.md diff --git a/Source/1-GraphicsAPI/05-3D/Source/main.cpp b/Source/1-GraphicsAPI/05-3D/Source/main.cpp new file mode 100644 index 00000000..fae40717 --- /dev/null +++ b/Source/1-GraphicsAPI/05-3D/Source/main.cpp @@ -0,0 +1,147 @@ +#include +#include "Render/RHI/QRhiWindow.h" +#include "Utils/CubeData.h" +#include "QDateTime" + +struct UniformBlock { + QGenericMatrix<4, 4, float> MVP; +}; + +class MyWindow : public QRhiWindow { +public: + MyWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + } +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mVertexBuffer; + QScopedPointer mUniformBuffer; + + QScopedPointer mShaderBindings; + QScopedPointer mPipeline; + +protected: + void initRhiResource() { + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(CubeData))); + mVertexBuffer->create(); + + mUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); + mUniformBuffer->create(); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, mUniformBuffer.get()) + }); + mShaderBindings->create(); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + + mPipeline->setSampleCount(mSwapChain->sampleCount()); + mPipeline->setTopology(QRhiGraphicsPipeline::Triangles); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec3 inPosition; + layout(binding = 0) uniform UniformBlock{ + mat4 MVP; + }UBO; + out gl_PerVertex { vec4 gl_Position; }; + void main() + { + gl_Position = UBO.MVP * vec4(inPosition ,1.0f); + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(location = 0) out vec4 outFragColor; + void main() + { + outFragColor = vec4(1); + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 3 * sizeof(float) } + }); + + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float3, 0 } + }); + + mPipeline->setVertexInputLayout(inputLayout); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mPipeline->create(); + + mSigSubmit.request(); + } + + virtual void onRenderTick() override { + if (mSigInit.ensure()) { + initRhiResource(); + } + + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + if (mSigSubmit.ensure()) { + batch->uploadStaticBuffer(mVertexBuffer.get(), CubeData); + } + + QMatrix4x4 corrMatrix = mRhi->clipSpaceCorrMatrix(); //裁剪空间矫正矩阵 + + QMatrix4x4 project; + project.perspective(90.0, renderTarget->pixelSize().width() / (float)renderTarget->pixelSize().height(), 0.01, 1000); // 设置Fov为90度,传入FrameBuffer的宽高比,设置近平面距离为0.01,远平面距离为1000 + + QMatrix4x4 view; + view.lookAt(QVector3D(10, 0, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 0)); // 从位置(10,0,0) 看向 (0,0,0),视线的上向量为竖直向上(Y轴正方向) + + float time = QTime::currentTime().msecsSinceStartOfDay() / 1000.0f; //获取当前时间的秒数(浮点类型) + float factor = qAbs(qSin(time)); //利用正弦函数让Y值随时间在[0,1]之间变化 + QMatrix4x4 model; + model.translate(QVector3D(0, factor * 5, 0)); //随时间上下移动 + model.scale(factor); //随时间缩放 + model.rotate(time * 180, QVector3D(1, 1, 1)); //随时间旋转 + + UniformBlock ubo; + ubo.MVP = (corrMatrix * project * view * model).toGenericMatrix<4, 4>(); + batch->updateDynamicBuffer(mUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); + + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, batch); + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexBindings); + cmdBuffer->draw(36); + cmdBuffer->endPass(); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + MyWindow* window = new MyWindow(initParams); + window->resize({ 800,600 }); + window->show(); + + app.exec(); + delete window; + return 0; +} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/05-Texture/Source/main.cpp b/Source/1-GraphicsAPI/05-Texture/Source/main.cpp deleted file mode 100644 index 6c18de6c..00000000 --- a/Source/1-GraphicsAPI/05-Texture/Source/main.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include -#include "Render/RHI/QRhiWindow.h" - -static float VertexData[] = { - //position(xy) texture coord(uv) - 1.0f, 1.0f, 1.0f, 0.0f, - -1.0f, 1.0f, 0.0f, 0.0f, - 1.0f, -1.0f, 1.0f, 1.0f, - -1.0f, -1.0f, 0.0f, 1.0f -}; - -class MyFirstTextureWindow : public QRhiWindow { -public: - MyFirstTextureWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) { - sigInit.request(); - } -private: - QRhiEx::Signal sigInit; - QRhiEx::Signal sigSubmit; - - QImage mImage; - QScopedPointer mVertexBuffer; - QScopedPointer mTexture; - QScopedPointer mSapmler; - QScopedPointer mShaderBindings; - QScopedPointer mPipeline; -protected: - void initRhiResource() { - mImage = QImage(RESOURCE_DIR"/Image/Logo.png").convertedTo(QImage::Format_RGBA8888); - - mTexture.reset(mRhi->newTexture(QRhiTexture::RGBA8, mImage.size())); - mTexture->create(); - - mSapmler.reset(mRhi->newSampler( - QRhiSampler::Filter::Linear, - QRhiSampler::Filter::Linear, - QRhiSampler::Filter::Nearest, - QRhiSampler::AddressMode::Repeat, - QRhiSampler::AddressMode::Repeat, - QRhiSampler::AddressMode::Repeat - )); - mSapmler->create(); - - mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); - mVertexBuffer->create(); - - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - - mShaderBindings->setBindings({ - QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage, mTexture.get(),mSapmler.get()) - }); - - mShaderBindings->create(); - - mPipeline.reset(mRhi->newGraphicsPipeline()); - - QRhiGraphicsPipeline::TargetBlend targetBlend; - targetBlend.enable = false; - mPipeline->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); - - mPipeline->setSampleCount(mSwapChain->sampleCount()); - - mPipeline->setDepthTest(false); - mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); - mPipeline->setDepthWrite(false); - mPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip); - - QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 - layout(location = 0) in vec2 inPosition; - layout(location = 1) in vec2 inUV; - - layout(location = 0) out vec2 vUV; - - out gl_PerVertex { vec4 gl_Position; }; - - void main() - { - vUV = inUV; - gl_Position = vec4(inPosition,0.0f,1.0f); - } - )"); - Q_ASSERT(vs.isValid()); - - QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 - layout(location = 0) in vec2 vUV; - layout(location = 0) out vec4 outFragColor; - layout(binding = 0) uniform sampler2D inTexture; - void main() - { - outFragColor = texture(inTexture,vUV); - } - )"); - Q_ASSERT(fs.isValid()); - - mPipeline->setShaderStages({ - { QRhiShaderStage::Vertex, vs }, - { QRhiShaderStage::Fragment, fs } - }); - - QRhiVertexInputLayout inputLayout; - inputLayout.setBindings({ - { 4 * sizeof(float) } - }); - inputLayout.setAttributes({ - { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, - { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) } - }); - - mPipeline->setVertexInputLayout(inputLayout); - mPipeline->setShaderResourceBindings(mShaderBindings.get()); - mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); - mPipeline->create(); - - sigSubmit.request(); - } - - void submitRhiData(QRhiResourceUpdateBatch* resourceUpdates) { - resourceUpdates->uploadStaticBuffer(mVertexBuffer.get(), VertexData); - resourceUpdates->uploadTexture(mTexture.get(), mImage); - qDebug() << mImage; - } - - virtual void onRenderTick() override { - QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - if (sigInit.receive()) { - initRhiResource(); - } - - QRhiResourceUpdateBatch* resourceUpdates = nullptr; - if (sigSubmit.receive()) { - resourceUpdates = mRhi->nextResourceUpdateBatch(); - submitRhiData(resourceUpdates); - } - - const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - - currentCmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue, resourceUpdates); - - currentCmdBuffer->setGraphicsPipeline(mPipeline.get()); - currentCmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); - currentCmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); - currentCmdBuffer->setVertexInput(0, 1, &vertexBindings); - currentCmdBuffer->draw(4); - - currentCmdBuffer->endPass(); - } -}; - - -int main(int argc, char **argv){ - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::D3D11; - MyFirstTextureWindow* window = new MyFirstTextureWindow(initParams); - window->resize({ 800,600 }); - window->show(); - - app.exec(); - delete window; - return 0; -} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/06-Blend/README.md b/Source/1-GraphicsAPI/06-3DWithCamera/README.md similarity index 100% rename from Source/1-GraphicsAPI/06-Blend/README.md rename to Source/1-GraphicsAPI/06-3DWithCamera/README.md diff --git a/Source/1-GraphicsAPI/06-3DWithCamera/Source/main.cpp b/Source/1-GraphicsAPI/06-3DWithCamera/Source/main.cpp new file mode 100644 index 00000000..5f3bf042 --- /dev/null +++ b/Source/1-GraphicsAPI/06-3DWithCamera/Source/main.cpp @@ -0,0 +1,181 @@ +#include +#include "Render/RHI/QRhiWindow.h" +#include "QDateTime" +#include "Utils/QRhiCamera.h" + +struct UniformBlock { + QGenericMatrix<4, 4, float> MVP; +}; + +static float GridData[] = { + //xyz uv + -1.0f, 0.0f, -1.0f, 0.0f , 0.0f, + 1.0f, 0.0f, -1.0f, 100.0f, 0.0f, + 1.0f, 0.0f, 1.0f, 100.0f, 100.0f, //使用重复填充扩展UV + + 1.0f, 0.0f, 1.0f, 100.0f, 100.0f, + -1.0f, 0.0f, 1.0f, 0.0f, 100.0f, + -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, +}; + +class MyWindow : public QRhiWindow { +public: + MyWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + } +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mCamera; + QImage mImage; + QScopedPointer mTexture; + QScopedPointer mSampler; + QScopedPointer mVertexBuffer; + QScopedPointer mUniformBuffer; + QScopedPointer mShaderBindings; + QScopedPointer mPipeline; +protected: + void initRhiResource() { + mCamera.reset(new QRhiCamera); + mCamera->setupRhi(mRhi.get()); + mCamera->setupWindow(this); + mCamera->setPosition(QVector3D(0, 10, 0)); + + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(GridData))); + mVertexBuffer->create(); + + mUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); + mUniformBuffer->create(); + + mSampler.reset(mRhi->newSampler( + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::Nearest, + QRhiSampler::Filter::Linear, + QRhiSampler::AddressMode::Repeat, + QRhiSampler::AddressMode::Repeat, + QRhiSampler::AddressMode::Repeat + )); + mSampler->create(); + + mImage = QImage("Resources/Image/Grid.png").convertedTo(QImage::Format_RGBA8888); + mTexture.reset(mRhi->newTexture(QRhiTexture::RGBA8, mImage.size(), 1 , QRhiTexture::Flag::MipMapped|QRhiTexture::UsedWithGenerateMips )); + mTexture->create(); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::StageFlag::VertexStage, mUniformBuffer.get()), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::StageFlag::FragmentStage, mTexture.get(),mSampler.get()) + }); + mShaderBindings->create(); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + + mPipeline->setSampleCount(mSwapChain->sampleCount()); + mPipeline->setTopology(QRhiGraphicsPipeline::Triangles); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec3 inPosition; + layout(location = 1) in vec2 inUV; + layout(location = 0) out vec2 vUV; + layout(binding = 0) uniform UniformBlock{ + mat4 MVP; + }UBO; + + out gl_PerVertex { vec4 gl_Position; }; + + void main() + { + gl_Position = UBO.MVP * vec4(inPosition ,1.0f); + vUV = inUV; + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(location = 0) in vec2 vUV; + layout(location = 0) out vec4 outFragColor; + layout(binding = 1) uniform sampler2D uTexture; + void main() + { + outFragColor = texture(uTexture,vUV); + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 5 * sizeof(float) } + }); + + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, + { 0, 1, QRhiVertexInputAttribute::Float2, 3 * sizeof(float)} + }); + + mPipeline->setVertexInputLayout(inputLayout); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mPipeline->create(); + + mSigSubmit.request(); + } + + virtual void onRenderTick() override { + if (mSigInit.ensure()) { + initRhiResource(); + } + + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + if (mSigSubmit.ensure()) { + batch->uploadStaticBuffer(mVertexBuffer.get(), GridData); + batch->uploadTexture(mTexture.get(), mImage); + batch->generateMips(mTexture.get()); + } + + QMatrix4x4 project = mCamera->getProjectionMatrixWithCorr(); + + QMatrix4x4 view = mCamera->getViewMatrix(); + + QMatrix4x4 model; + model.scale(10000); + + UniformBlock ubo; + ubo.MVP = (project * view * model).toGenericMatrix<4, 4>(); + batch->updateDynamicBuffer(mUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, batch); + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexBindings); + cmdBuffer->draw(6); + cmdBuffer->endPass(); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + MyWindow* window = new MyWindow(initParams); + window->resize({ 800,600 }); + window->show(); + + app.exec(); + delete window; + return 0; +} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/06-Blend/Source/main.cpp b/Source/1-GraphicsAPI/06-Blend/Source/main.cpp deleted file mode 100644 index 20ea8f3b..00000000 --- a/Source/1-GraphicsAPI/06-Blend/Source/main.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#include -#include -#include -#include "Render/RHI/QRhiWindow.h" - -static float VertexData1[] = { - //position(xyz) color(rgb) - 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, - -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, - 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, -}; - -static float VertexData2[] = { - //position(xy) color(rgb) - 0.2f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, - -0.3f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, - 0.7f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, -}; - -class BlendWindow : public QRhiWindow { -private: - QRhiEx::Signal sigInit; - QRhiEx::Signal sigSubmit; - - QScopedPointer mShaderBindings; - - QScopedPointer mVertexBuffer1; - QScopedPointer mPipeline1; - - QScopedPointer mVertexBuffer2; - QScopedPointer mPipeline2; -public: - BlendWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) { - sigInit.request(); - sigSubmit.request(); - } -protected: - virtual void onRenderTick() override { - QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - if (sigInit.receive()) { - initRhiResource(); - } - QRhiResourceUpdateBatch* resourceUpdates = nullptr; - if (sigSubmit.receive()) { - resourceUpdates = mRhi->nextResourceUpdateBatch(); - submitRhiData(resourceUpdates); - } - - const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - - currentCmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue, resourceUpdates); - - currentCmdBuffer->setGraphicsPipeline(mPipeline1.get()); - currentCmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); - currentCmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings1(mVertexBuffer1.get(), 0); - currentCmdBuffer->setVertexInput(0, 1, &vertexBindings1); - currentCmdBuffer->draw(3); - - currentCmdBuffer->setGraphicsPipeline(mPipeline2.get()); - currentCmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); - currentCmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings2(mVertexBuffer2.get(), 0); - currentCmdBuffer->setVertexInput(0, 1, &vertexBindings2); - currentCmdBuffer->draw(3); - - currentCmdBuffer->endPass(); - } - - void initRhiResource() { - mVertexBuffer1.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData1))); - mVertexBuffer1->create(); - - mVertexBuffer2.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData2))); - mVertexBuffer2->create(); - - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - mShaderBindings->create(); - - QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 -layout(location = 0) in vec3 position; -layout(location = 1) in vec4 color; -layout(location = 0) out vec4 v_color; -out gl_PerVertex { - vec4 gl_Position; -}; -void main(){ - v_color = color; - gl_Position = vec4(position,1.0f); -} -)"); - Q_ASSERT(vs.isValid()); - - QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 -layout(location = 0) in vec4 v_color; -layout(location = 0) out vec4 fragColor; -void main(){ - fragColor = v_color; -} -)"); - Q_ASSERT(fs.isValid()); - - QRhiVertexInputLayout inputLayout; - inputLayout.setBindings({ - QRhiVertexInputBinding(7 * sizeof(float)) - }); - - inputLayout.setAttributes({ - QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float3, 0), - QRhiVertexInputAttribute(0, 1, QRhiVertexInputAttribute::Float4, 3 * sizeof(float)) - }); - - mPipeline1.reset(mRhi->newGraphicsPipeline()); - - mPipeline1->setSampleCount(mSwapChain->sampleCount()); - - mPipeline1->setShaderStages({ - QRhiShaderStage(QRhiShaderStage::Vertex, vs), - QRhiShaderStage(QRhiShaderStage::Fragment, fs) - }); - - mPipeline1->setVertexInputLayout(inputLayout); - mPipeline1->setShaderResourceBindings(mShaderBindings.get()); - mPipeline1->setRenderPassDescriptor(mSwapChainPassDesc.get()); - - - mPipeline2.reset(mRhi->newGraphicsPipeline()); - mPipeline2->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); - - mPipeline2->setSampleCount(mSwapChain->sampleCount()); - mPipeline2->setShaderStages({ - QRhiShaderStage(QRhiShaderStage::Vertex, vs), - QRhiShaderStage(QRhiShaderStage::Fragment, fs) - }); - - mPipeline2->setVertexInputLayout(inputLayout); - mPipeline2->setShaderResourceBindings(mShaderBindings.get()); - mPipeline2->setRenderPassDescriptor(mSwapChainPassDesc.get()); - - QRhiGraphicsPipeline::TargetBlend targetBlend1; - targetBlend1.enable = true; - mPipeline1->setTargetBlends({ targetBlend1 }); - - QRhiGraphicsPipeline::TargetBlend targetBlend2; - targetBlend2.enable = true; - targetBlend2.colorWrite = QRhiGraphicsPipeline::ColorMaskComponent::B | QRhiGraphicsPipeline::ColorMaskComponent::A; - mPipeline2->setTargetBlends({ targetBlend2 }); - - mPipeline1->create(); - mPipeline2->create(); - } - - void submitRhiData(QRhiResourceUpdateBatch* resourceUpdates) { - resourceUpdates->uploadStaticBuffer(mVertexBuffer1.get(), VertexData1); - resourceUpdates->uploadStaticBuffer(mVertexBuffer2.get(), VertexData2); - } -}; - -int main(int argc, char **argv) -{ - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - BlendWindow window(initParams); - window.resize({ 800,600 }); - window.show(); - app.exec(); - return 0; -} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/07-Test/README.md b/Source/1-GraphicsAPI/07-ScissorTest/README.md similarity index 100% rename from Source/1-GraphicsAPI/07-Test/README.md rename to Source/1-GraphicsAPI/07-ScissorTest/README.md diff --git a/Source/1-GraphicsAPI/07-ScissorTest/Source/main.cpp b/Source/1-GraphicsAPI/07-ScissorTest/Source/main.cpp new file mode 100644 index 00000000..87acd60b --- /dev/null +++ b/Source/1-GraphicsAPI/07-ScissorTest/Source/main.cpp @@ -0,0 +1,120 @@ +#include +#include "QEngineApplication.h" +#include "Render/RHI/QRhiWindow.h" + +static float VertexData[] = { + //position(xy) color(rgba) + 0.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, + -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, +}; + +class TriangleWindow : public QRhiWindow { +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + QScopedPointer mVertexBuffer; + QScopedPointer mShaderBindings; + QScopedPointer mPipeline; +public: + TriangleWindow(QRhiHelper::InitParams inInitParams) + : QRhiWindow(inInitParams) { + mSigInit.request(); + mSigSubmit.request(); + } +protected: + virtual void onRenderTick() override { + if (mSigInit.ensure()) { //初始化资源 + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + mVertexBuffer->create(); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + QRhiVertexInputBinding(6 * sizeof(float)) + }); + + inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0 , QRhiVertexInputAttribute::Float2, 0), + QRhiVertexInputAttribute(0, 1 , QRhiVertexInputAttribute::Float4, sizeof(float) * 2), + }); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + + mPipeline->setVertexInputLayout(inputLayout); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->create(); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->setSampleCount(mSwapChain->sampleCount()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; //这里需要与上面的inputLayout 对应 + layout(location = 1) in vec4 color; + + layout (location = 0) out vec4 vColor; //输出变量到 fragment shader 中,这里的location是out的,而不是in + + out gl_PerVertex { + vec4 gl_Position; + }; + + void main(){ + gl_Position = vec4(position,0.0f,1.0f); + vColor = color; + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout (location = 0) in vec4 vColor; //上一阶段的out变成了这一阶段的in + layout (location = 0) out vec4 fragColor; //片段着色器输入 + void main(){ + fragColor = vColor; + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + QRhiShaderStage(QRhiShaderStage::Vertex, vs), + QRhiShaderStage(QRhiShaderStage::Fragment, fs) + }); + + mPipeline->setFlags(QRhiGraphicsPipeline::Flag::UsesScissor); //开启流水线的裁剪测试功能 + mPipeline->create(); + } + + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + if (mSigSubmit.ensure()) { + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + cmdBuffer->resourceUpdate(batch); + } + + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, nullptr); + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setScissor(QRhiScissor(0, 0, mSwapChain->currentPixelSize().width() / 2, mSwapChain->currentPixelSize().height() / 2)); //通过裁剪只保留四分之一边角区域,具体的表现情况跟不同API的标准坐标空间有关 + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexInput(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexInput); + cmdBuffer->draw(3); + + cmdBuffer->endPass(); + } +}; + +int main(int argc, char **argv) +{ + QEngineApplication app(argc, argv); + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + TriangleWindow window(initParams); + window.resize({ 800,600 }); + window.show(); + app.exec(); + return 0; +} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/07-Test/Source/main.cpp b/Source/1-GraphicsAPI/07-Test/Source/main.cpp deleted file mode 100644 index 1c0e093a..00000000 --- a/Source/1-GraphicsAPI/07-Test/Source/main.cpp +++ /dev/null @@ -1,178 +0,0 @@ -#include -#include -#include -#include "Render/RHI/QRhiWindow.h" - -static float VertexData1[] = { - //position(xyz) color(rgb) - 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, - -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, - 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, -}; - -static float VertexData2[] = { - //position(xy) color(rgb) - 0.2f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, - -0.3f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, - 0.7f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, -}; - -class DepthStencilTestWindow : public QRhiWindow { -private: - QRhiEx::Signal sigInit; - QRhiEx::Signal sigSubmit; - - QScopedPointer mShaderBindings; - - QScopedPointer mVertexBuffer1; - QScopedPointer mPipeline1; - - QScopedPointer mVertexBuffer2; - QScopedPointer mPipeline2; -public: - DepthStencilTestWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) { - sigInit.request(); - sigSubmit.request(); - } -protected: - virtual void onRenderTick() override { - QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - if (sigInit.receive()) { - initRhiResource(); - } - QRhiResourceUpdateBatch* resourceUpdates = nullptr; - if (sigSubmit.receive()) { - resourceUpdates = mRhi->nextResourceUpdateBatch(); - submitRhiData(resourceUpdates); - } - - const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - - currentCmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue, resourceUpdates); - - currentCmdBuffer->setGraphicsPipeline(mPipeline1.get()); - currentCmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); - currentCmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings1(mVertexBuffer1.get(), 0); - currentCmdBuffer->setVertexInput(0, 1, &vertexBindings1); - currentCmdBuffer->draw(3); - - currentCmdBuffer->setGraphicsPipeline(mPipeline2.get()); - currentCmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); - currentCmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings2(mVertexBuffer2.get(), 0); - currentCmdBuffer->setVertexInput(0, 1, &vertexBindings2); - currentCmdBuffer->draw(3); - - currentCmdBuffer->endPass(); - } - - void initRhiResource() { - mVertexBuffer1.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData1))); - mVertexBuffer1->create(); - - mVertexBuffer2.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData2))); - mVertexBuffer2->create(); - - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - mShaderBindings->create(); - - QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 -layout(location = 0) in vec3 position; -layout(location = 1) in vec4 color; -layout(location = 0) out vec4 v_color; -out gl_PerVertex { - vec4 gl_Position; -}; -void main(){ - v_color = color; - gl_Position = vec4(position,1.0f); -} -)"); - Q_ASSERT(vs.isValid()); - - QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 -layout(location = 0) in vec4 v_color; -layout(location = 0) out vec4 fragColor; -void main(){ - fragColor = v_color; -} -)"); - Q_ASSERT(fs.isValid()); - - QRhiGraphicsPipeline::TargetBlend targetBlend; - targetBlend.enable = false; - - QRhiVertexInputLayout inputLayout; - inputLayout.setBindings({ - QRhiVertexInputBinding(7 * sizeof(float)) - }); - - inputLayout.setAttributes({ - QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float3, 0), - QRhiVertexInputAttribute(0, 1, QRhiVertexInputAttribute::Float4, 3 * sizeof(float)) - }); - - mPipeline1.reset(mRhi->newGraphicsPipeline()); - - mPipeline1->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); - - mPipeline1->setSampleCount(mSwapChain->sampleCount()); - - mPipeline1->setShaderStages({ - QRhiShaderStage(QRhiShaderStage::Vertex, vs), - QRhiShaderStage(QRhiShaderStage::Fragment, fs) - }); - - mPipeline1->setVertexInputLayout(inputLayout); - mPipeline1->setShaderResourceBindings(mShaderBindings.get()); - mPipeline1->setRenderPassDescriptor(mSwapChainPassDesc.get()); - - - mPipeline2.reset(mRhi->newGraphicsPipeline()); - mPipeline2->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); - - mPipeline2->setSampleCount(mSwapChain->sampleCount()); - mPipeline2->setShaderStages({ - QRhiShaderStage(QRhiShaderStage::Vertex, vs), - QRhiShaderStage(QRhiShaderStage::Fragment, fs) - }); - - mPipeline2->setVertexInputLayout(inputLayout); - mPipeline2->setShaderResourceBindings(mShaderBindings.get()); - mPipeline2->setRenderPassDescriptor(mSwapChainPassDesc.get()); - - mPipeline1->setDepthTest(false); - mPipeline1->setDepthOp(QRhiGraphicsPipeline::Always); - mPipeline1->setDepthWrite(false); - mPipeline1->setStencilTest(false); - - mPipeline2->setDepthTest(false); - mPipeline2->setDepthOp(QRhiGraphicsPipeline::Always); - mPipeline2->setDepthWrite(false); - mPipeline2->setStencilTest(false); - - mPipeline1->create(); - mPipeline2->create(); - } - - void submitRhiData(QRhiResourceUpdateBatch* resourceUpdates) { - resourceUpdates->uploadStaticBuffer(mVertexBuffer1.get(), VertexData1); - resourceUpdates->uploadStaticBuffer(mVertexBuffer2.get(), VertexData2); - } -}; - -int main(int argc, char **argv) -{ - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - DepthStencilTestWindow window(initParams); - window.resize({ 800,600 }); - window.show(); - app.exec(); - return 0; -} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/08-Instancing/Source/main.cpp b/Source/1-GraphicsAPI/08-Instancing/Source/main.cpp deleted file mode 100644 index 70e48bd9..00000000 --- a/Source/1-GraphicsAPI/08-Instancing/Source/main.cpp +++ /dev/null @@ -1,146 +0,0 @@ -#include -#include "Render/RHI/QRhiWindow.h" - -static float VertexData[] = { - //position (xy) - 0.0f, 0.1f, - -0.1f, -0.1f, - 0.1f, -0.1f, -}; - -static float InstancingData[] = { - //offset (xy) - 0.0f, 0.5f, - -0.5f, -0.5f, - 0.5f, -0.5f, -}; - -class InstancingWindow : public QRhiWindow { -private: - QRhiEx::Signal sigInit; - QRhiEx::Signal sigSubmit; - - QScopedPointer mVertexBuffer; - QScopedPointer mInstancingBuffer; - QScopedPointer mShaderBindings; - QScopedPointer mPipeline; -public: - InstancingWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) { - sigInit.request(); - sigSubmit.request(); - } -protected: - virtual void onRenderTick() override { - QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - if (sigInit.receive()) { - initRhiResource(); - } - QRhiResourceUpdateBatch* resourceUpdates = nullptr; - if (sigSubmit.receive()) { - resourceUpdates = mRhi->nextResourceUpdateBatch(); - submitRhiData(resourceUpdates); - } - - const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - - currentCmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue, resourceUpdates); - - currentCmdBuffer->setGraphicsPipeline(mPipeline.get()); - currentCmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); - currentCmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings[] = { - { mVertexBuffer.get(), 0 }, - { mInstancingBuffer.get(), 0 }, - }; - currentCmdBuffer->setVertexInput(0, 2, vertexBindings); - currentCmdBuffer->draw(3, 3); - - currentCmdBuffer->endPass(); - } - - void initRhiResource() { - mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); - mVertexBuffer->create(); - - mInstancingBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(InstancingData))); - mInstancingBuffer->create(); - - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - mShaderBindings->create(); - - mPipeline.reset(mRhi->newGraphicsPipeline()); - - QRhiGraphicsPipeline::TargetBlend targetBlend; - targetBlend.enable = false; - mPipeline->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); - - mPipeline->setSampleCount(mSwapChain->sampleCount()); - - mPipeline->setDepthTest(false); - mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); - mPipeline->setDepthWrite(false); - - QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 - layout(location = 0) in vec2 position; - layout(location = 1) in vec2 offset; - - out gl_PerVertex { - vec4 gl_Position; - }; - void main(){ - gl_Position = vec4(position + offset,0.0f,1.0f); - } - )"); - Q_ASSERT(vs.isValid()); - - QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 - layout(location = 0) out vec4 fragColor; - void main(){ - fragColor = vec4(0.1f,0.5f,0.9f,1.0f); - } - )"); - Q_ASSERT(fs.isValid()); - - mPipeline->setShaderStages({ - { QRhiShaderStage::Vertex, vs }, - { QRhiShaderStage::Fragment, fs } - }); - - QRhiVertexInputLayout inputLayout; - inputLayout.setBindings({ - QRhiVertexInputBinding(2 * sizeof(float)), - QRhiVertexInputBinding(2 * sizeof(float), QRhiVertexInputBinding::PerInstance), - }); - - inputLayout.setAttributes({ - QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0), - QRhiVertexInputAttribute(1, 1, QRhiVertexInputAttribute::Float2, 0), - }); - - mPipeline->setVertexInputLayout(inputLayout); - mPipeline->setShaderResourceBindings(mShaderBindings.get()); - mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); - mPipeline->create(); - } - - void submitRhiData(QRhiResourceUpdateBatch* resourceUpdates) { - resourceUpdates->uploadStaticBuffer(mVertexBuffer.get(), VertexData); - resourceUpdates->uploadStaticBuffer(mInstancingBuffer.get(), InstancingData); - } -}; -int main(int argc, char **argv) -{ - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::D3D11; - InstancingWindow* window = new InstancingWindow(initParams); - window->resize({ 800,600 }); - window->show(); - app.exec(); - delete window; - return 0; -} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/08-Instancing/README.md b/Source/1-GraphicsAPI/08-StencilTest/README.md similarity index 100% rename from Source/1-GraphicsAPI/08-Instancing/README.md rename to Source/1-GraphicsAPI/08-StencilTest/README.md diff --git a/Source/1-GraphicsAPI/08-StencilTest/Source/main.cpp b/Source/1-GraphicsAPI/08-StencilTest/Source/main.cpp new file mode 100644 index 00000000..43e68518 --- /dev/null +++ b/Source/1-GraphicsAPI/08-StencilTest/Source/main.cpp @@ -0,0 +1,160 @@ +#include +#include +#include "QEngineApplication.h" +#include "Render/RHI/QRhiWindow.h" + +static QVector2D Vertices[3] = { + { 0.0f, 0.5f}, + {-0.5f, -0.5f}, + { 0.5f, -0.5f}, +}; + +struct UniformBlock { + QGenericMatrix<4, 4, float> MVP; +}; + +class TriangleWindow : public QRhiWindow { +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mMaskVertexBuffer; + QScopedPointer mMaskPipeline; + + QScopedPointer mVertexBuffer; + QScopedPointer mShaderBindings; + QScopedPointer mPipeline; +public: + TriangleWindow(QRhiHelper::InitParams inInitParams) + : QRhiWindow(inInitParams) { + mSigInit.request(); + mSigSubmit.request(); + } +protected: + virtual void onRenderTick() override { + if (mSigInit.ensure()) { + mMaskVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(Vertices))); + mMaskVertexBuffer->create(); + + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(Vertices))); + mVertexBuffer->create(); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + QRhiVertexInputBinding(sizeof(QVector2D)) + }); + + inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0 , QRhiVertexInputAttribute::Float2, 0), + }); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; + out gl_PerVertex { + vec4 gl_Position; + }; + void main(){ + gl_Position = vec4(position,0.0f,1.0f); + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout (location = 0) out vec4 fragColor; + void main(){ + fragColor = vec4(1); + } + )"); + Q_ASSERT(fs.isValid()); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->create(); + + mMaskPipeline.reset(mRhi->newGraphicsPipeline()); + mMaskPipeline->setVertexInputLayout(inputLayout); + mMaskPipeline->setShaderResourceBindings(mShaderBindings.get()); + mMaskPipeline->setSampleCount(mSwapChain->sampleCount()); + mMaskPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mMaskPipeline->setShaderStages({ + QRhiShaderStage(QRhiShaderStage::Vertex, vs), + QRhiShaderStage(QRhiShaderStage::Fragment, fs) + }); + mMaskPipeline->setFlags(QRhiGraphicsPipeline::Flag::UsesStencilRef); //ˮߵģԹ + mMaskPipeline->setStencilTest(true); //ģ + QRhiGraphicsPipeline::StencilOpState maskStencilState; + maskStencilState.compareOp = QRhiGraphicsPipeline::CompareOp::Never; //ùڻģ棨֣DzϣRTϻκƬɫƬԶͨģ + maskStencilState.failOp = QRhiGraphicsPipeline::StencilOp::Replace; //õƬûͨģʱʹStencilRefģ滺 + mMaskPipeline->setStencilFront(maskStencilState); //ָģ + mMaskPipeline->setStencilBack(maskStencilState); //ָģ + mMaskPipeline->create(); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + mPipeline->setVertexInputLayout(inputLayout); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->setSampleCount(mSwapChain->sampleCount()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mPipeline->setShaderStages({ + QRhiShaderStage(QRhiShaderStage::Vertex, vs), + QRhiShaderStage(QRhiShaderStage::Fragment, fs) + }); + mPipeline->setFlags(QRhiGraphicsPipeline::Flag::UsesStencilRef); //ˮߵģԹ + mPipeline->setStencilTest(true); //ģ + QRhiGraphicsPipeline::StencilOpState stencilState; + stencilState.compareOp = QRhiGraphicsPipeline::CompareOp::Equal; //ϣڵǰߵStencilRefģ滺ϵƬֵʱͨģ + stencilState.passOp = QRhiGraphicsPipeline::StencilOp::Keep; //ͨԺ󲻻ģ滺иֵ + stencilState.failOp = QRhiGraphicsPipeline::StencilOp::Keep; //ûͨʱҲģ滺иֵ + mPipeline->setStencilFront(stencilState); + mPipeline->setStencilBack(stencilState); + mPipeline->create(); + + } + + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + if (mSigSubmit.ensure()) { + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + batch->uploadStaticBuffer(mMaskVertexBuffer.get(), Vertices); //ϴģ棨֣Ķݣһ + + for (auto& vertex : Vertices) //ϴʵͼεĶݣһ·ת + vertex.setY(-vertex.y()); + batch->uploadStaticBuffer(mVertexBuffer.get(), Vertices); + + cmdBuffer->resourceUpdate(batch); + } + + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; //ʹ 0 ģ滺 + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, nullptr); + + cmdBuffer->setGraphicsPipeline(mMaskPipeline.get()); + cmdBuffer->setStencilRef(1); //StencilRefΪ1ù߻ģ滺һֵΪ1 + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput maskVertexInput(mMaskVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &maskVertexInput); + cmdBuffer->draw(3); + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setStencilRef(1); //StencilRefΪ1ù߻1ӦλõƬģֵбȽϣʱŻͨģԣҲǻὫƬλƵɫ + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexInput(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexInput); + cmdBuffer->draw(3); + + cmdBuffer->endPass(); + } +}; + +int main(int argc, char** argv) +{ + QEngineApplication app(argc, argv); + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + TriangleWindow window(initParams); + window.resize({ 800,600 }); + window.show(); + app.exec(); + return 0; +} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/09-MultiRenderTarget/README.md b/Source/1-GraphicsAPI/09-DepthTest/README.md similarity index 100% rename from Source/1-GraphicsAPI/09-MultiRenderTarget/README.md rename to Source/1-GraphicsAPI/09-DepthTest/README.md diff --git a/Source/1-GraphicsAPI/09-DepthTest/Source/main.cpp b/Source/1-GraphicsAPI/09-DepthTest/Source/main.cpp new file mode 100644 index 00000000..cc79a4c3 --- /dev/null +++ b/Source/1-GraphicsAPI/09-DepthTest/Source/main.cpp @@ -0,0 +1,174 @@ +#include +#include "Render/RHI/QRhiWindow.h" +#include "Utils/CubeData.h" +#include "QDateTime" + +struct UniformBlock { + QVector4D color; + QGenericMatrix<4, 4, float> MVP; +}; + +class MyWindow : public QRhiWindow { +public: + MyWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + } +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mCubeVertexBuffer; + QScopedPointer mCubeUniformBuffer; + QScopedPointer mCubeShaderBindings; + + QScopedPointer mPlaneVertexBuffer; + QScopedPointer mPlaneUniformBuffer; + QScopedPointer mPlaneShaderBindings; + + QScopedPointer mPipeline; +protected: + void initRhiResource() { + mCubeVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(CubeData))); + mCubeVertexBuffer->create(); + + mPlaneVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(PlaneData))); + mPlaneVertexBuffer->create(); + + mCubeUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); + mCubeUniformBuffer->create(); + + mCubeShaderBindings.reset(mRhi->newShaderResourceBindings()); + mCubeShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, mCubeUniformBuffer.get()) + }); + mCubeShaderBindings->create(); + + mPlaneUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); + mPlaneUniformBuffer->create(); + + mPlaneShaderBindings.reset(mRhi->newShaderResourceBindings()); + mPlaneShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, mPlaneUniformBuffer.get()) + }); + mPlaneShaderBindings->create(); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + mPipeline->setSampleCount(mSwapChain->sampleCount()); + mPipeline->setTopology(QRhiGraphicsPipeline::Triangles); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec3 inPosition; + layout(binding = 0) uniform UniformBlock{ + vec4 color; + mat4 MVP; + }UBO; + out gl_PerVertex { vec4 gl_Position; }; + void main() + { + gl_Position = UBO.MVP * vec4(inPosition ,1.0f); + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(location = 0) out vec4 outFragColor; + layout(binding = 0) uniform UniformBlock{ + vec4 color; + mat4 MVP; + }UBO; + void main() + { + outFragColor = UBO.color; + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 3 * sizeof(float) } + }); + + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float3, 0 } + }); + + mPipeline->setVertexInputLayout(inputLayout); + mPipeline->setShaderResourceBindings(mCubeShaderBindings.get()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mPipeline->setDepthTest(true); + mPipeline->setDepthWrite(true); + mPipeline->create(); + + mSigSubmit.request(); + } + + virtual void onRenderTick() override { + if (mSigInit.ensure()) { + initRhiResource(); + } + + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + if (mSigSubmit.ensure()) { + batch->uploadStaticBuffer(mCubeVertexBuffer.get(), CubeData); + batch->uploadStaticBuffer(mPlaneVertexBuffer.get(), PlaneData); + } + + QMatrix4x4 project = mRhi->clipSpaceCorrMatrix(); + + project.perspective(90.0, renderTarget->pixelSize().width() / (float)renderTarget->pixelSize().height(), 0.01, 1000); + QMatrix4x4 view; + view.lookAt(QVector3D(5, 5, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 0)); + + QMatrix4x4 model; + UniformBlock ubo; + ubo.color = QVector4D(0, 0, 1, 1); + ubo.MVP = (project * view * model).toGenericMatrix<4, 4>(); + batch->updateDynamicBuffer(mCubeUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); + + ubo.color = QVector4D(1, 0, 0, 1); + ubo.MVP = (project * view * model).toGenericMatrix<4, 4>(); + batch->updateDynamicBuffer(mPlaneUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); + + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, batch); + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(mCubeShaderBindings.get()); + const QRhiCommandBuffer::VertexInput cubeVertexBindings(mCubeVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &cubeVertexBindings); + cmdBuffer->draw(36); + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(mPlaneShaderBindings.get()); + const QRhiCommandBuffer::VertexInput planeVertexBindings(mPlaneVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &planeVertexBindings); + cmdBuffer->draw(6); + + cmdBuffer->endPass(); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + MyWindow window(initParams); + window.resize({ 800,600 }); + window.show(); + + return app.exec(); +} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/09-MultiRenderTarget/Source/main.cpp b/Source/1-GraphicsAPI/09-MultiRenderTarget/Source/main.cpp deleted file mode 100644 index 9a779e2a..00000000 --- a/Source/1-GraphicsAPI/09-MultiRenderTarget/Source/main.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include - -#include "Render/RHI/QRhiWindow.h" -#include "Render/Painter/TexturePainter.h" - -static float VertexData[] = { - //position(xy) - 0.0f, 0.5f, - -0.5f, -0.5f, - 0.5f, -0.5f, -}; - -class MRTWindow : public QRhiWindow { -private: - QRhiEx::Signal sigInit; - QRhiEx::Signal sigSubmit; - - QScopedPointer mVertexBuffer; - QScopedPointer mShaderBindings; - QScopedPointer mPipeline; - - QScopedPointer mTexture0; - QScopedPointer mTexture1; - QScopedPointer mRenderTarget; - QScopedPointer mRenderPassDesc; - - QScopedPointer mTexturePainter; - -public: - MRTWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) { - sigInit.request(); - sigSubmit.request(); - } -protected: - virtual void onRenderTick() override { - QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); - if (sigInit.receive()) { - initRhiResource(); - } - QRhiResourceUpdateBatch* resourceUpdates = nullptr; - if (sigSubmit.receive()) { - resourceUpdates = mRhi->nextResourceUpdateBatch(); - resourceUpdates->uploadStaticBuffer(mVertexBuffer.get(), VertexData); - } - - const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - - currentCmdBuffer->beginPass(mRenderTarget.get(), clearColor, dsClearValue, resourceUpdates); - - currentCmdBuffer->setGraphicsPipeline(mPipeline.get()); - currentCmdBuffer->setViewport(QRhiViewport(0, 0, mRenderTarget->pixelSize().width(), mRenderTarget->pixelSize().height())); - currentCmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); - currentCmdBuffer->setVertexInput(0, 1, &vertexBindings); - currentCmdBuffer->draw(3); - - currentCmdBuffer->endPass(); - - static int counter = 0; - static QRhiTexture* CurrentTexture = nullptr; - if (counter && counter % 60 == 0) { - if (!CurrentTexture || CurrentTexture == mTexture1.get()) - CurrentTexture = mTexture0.get(); - else { - CurrentTexture = mTexture1.get(); - } - mTexturePainter->setupTexture(CurrentTexture); - mTexturePainter->compile(); - counter = 0; - } - counter++; - - currentCmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue); - mTexturePainter->paint(currentCmdBuffer, currentRenderTarget); - currentCmdBuffer->endPass(); - - } - - void initRhiResource() { - mTexture0.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(100, 100), 1, QRhiTexture::Flag::RenderTarget | QRhiTexture::UsedAsTransferSource)); - mTexture1.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(100, 100), 1, QRhiTexture::Flag::RenderTarget | QRhiTexture::UsedAsTransferSource)); - mTexture0->create(); - mTexture1->create(); - - QRhiTextureRenderTargetDescription rtDesc; - rtDesc.setColorAttachments({ mTexture0.get(),mTexture1.get() }); - mRenderTarget.reset(mRhi->newTextureRenderTarget(rtDesc)); - mRenderPassDesc.reset(mRenderTarget->newCompatibleRenderPassDescriptor()); - mRenderTarget->setRenderPassDescriptor(mRenderPassDesc.get()); - mRenderTarget->create(); - - mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); - mVertexBuffer->create(); - - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - mShaderBindings->create(); - - mPipeline.reset(mRhi->newGraphicsPipeline()); - - QRhiGraphicsPipeline::TargetBlend targetBlend; - targetBlend.enable = false; - mPipeline->setTargetBlends({ targetBlend,targetBlend }); - - mPipeline->setSampleCount(mRenderTarget->sampleCount()); - - mPipeline->setDepthTest(false); - mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); - mPipeline->setDepthWrite(false); - - QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 -layout(location = 0) in vec2 position; -out gl_PerVertex { - vec4 gl_Position; -}; -void main(){ - gl_Position = vec4(position,0.0f,1.0f); -} -)"); - Q_ASSERT(vs.isValid()); - - QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 -layout(location = 0) out vec4 fragColor0; -layout(location = 1) out vec4 fragColor1; -void main(){ - fragColor0 = vec4(1.0f,0.0f,0.0f,1.0f); - fragColor1 = vec4(0.0f,0.0f,1.0f,1.0f); -} -)"); - Q_ASSERT(fs.isValid()); - - mPipeline->setShaderStages({ - QRhiShaderStage(QRhiShaderStage::Vertex, vs), - QRhiShaderStage(QRhiShaderStage::Fragment, fs) - }); - - QRhiVertexInputLayout inputLayout; - inputLayout.setBindings({ - QRhiVertexInputBinding(2 * sizeof(float)) - }); - - inputLayout.setAttributes({ - QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0), - }); - - mPipeline->setVertexInputLayout(inputLayout); - mPipeline->setShaderResourceBindings(mShaderBindings.get()); - mPipeline->setRenderPassDescriptor(mRenderPassDesc.get()); - mPipeline->create(); - - mTexturePainter.reset(new TexturePainter); - mTexturePainter->setupRhi(mRhi.get()); - mTexturePainter->setupTexture(mTexture0.get()); - mTexturePainter->setupRenderPassDesc(mSwapChain->renderPassDescriptor()); - mTexturePainter->setupSampleCount(mSwapChain->sampleCount()); - mTexturePainter->compile(); - } -}; - -int main(int argc, char **argv) -{ - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - MRTWindow window(initParams); - window.resize({ 800,600 }); - window.show(); - app.exec(); - return 0; -} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/10-Offscreen/README.md b/Source/1-GraphicsAPI/10-Blend/README.md similarity index 100% rename from Source/1-GraphicsAPI/10-Offscreen/README.md rename to Source/1-GraphicsAPI/10-Blend/README.md diff --git a/Source/1-GraphicsAPI/10-Blend/Source/main.cpp b/Source/1-GraphicsAPI/10-Blend/Source/main.cpp new file mode 100644 index 00000000..48e097ee --- /dev/null +++ b/Source/1-GraphicsAPI/10-Blend/Source/main.cpp @@ -0,0 +1,198 @@ +#include +#include "Render/RHI/QRhiWindow.h" +#include "Utils/CubeData.h" +#include "QDateTime" + +struct UniformBlock { + QVector4D color; + QGenericMatrix<4, 4, float> MVP; +}; + +class MyWindow : public QRhiWindow { +public: + MyWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + } +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mCubeVertexBuffer; + + QScopedPointer mPlaneVertexBuffer; + + QScopedPointer mCubeUniformBuffer; + QScopedPointer mCubeShaderBindings; + + QScopedPointer mPlaneUniformBuffer; + QScopedPointer mPlaneShaderBindings; + + QScopedPointer mPlanePipeline; + QScopedPointer mCubePipeline; +protected: + void initRhiResource() { + mCubeVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(CubeData))); + mCubeVertexBuffer->create(); + + mPlaneVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(PlaneData))); + mPlaneVertexBuffer->create(); + + mCubeUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); + mCubeUniformBuffer->create(); + + mCubeShaderBindings.reset(mRhi->newShaderResourceBindings()); + mCubeShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, mCubeUniformBuffer.get()) + }); + mCubeShaderBindings->create(); + + mPlaneUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); + mPlaneUniformBuffer->create(); + + mPlaneShaderBindings.reset(mRhi->newShaderResourceBindings()); + mPlaneShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, mPlaneUniformBuffer.get()) + }); + mPlaneShaderBindings->create(); + + mCubePipeline.reset(mRhi->newGraphicsPipeline()); + mCubePipeline->setSampleCount(mSwapChain->sampleCount()); + mCubePipeline->setTopology(QRhiGraphicsPipeline::Triangles); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec3 inPosition; + layout(binding = 0) uniform UniformBlock{ + vec4 color; + mat4 MVP; + }UBO; + out gl_PerVertex { vec4 gl_Position; }; + void main() + { + gl_Position = UBO.MVP * vec4(inPosition ,1.0f); + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(location = 0) out vec4 outFragColor; + layout(binding = 0) uniform UniformBlock{ + vec4 color; + mat4 MVP; + }UBO; + void main() + { + outFragColor = UBO.color; + } + )"); + Q_ASSERT(fs.isValid()); + + mCubePipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 3 * sizeof(float) } + }); + + inputLayout.setAttributes({ + { 0, 0, QRhiVertexInputAttribute::Float3, 0 } + }); + + QRhiGraphicsPipeline::TargetBlend blend; + blend.enable = true; + blend.srcColor = QRhiGraphicsPipeline::SrcAlpha; + blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha; + + mCubePipeline->setVertexInputLayout(inputLayout); + mCubePipeline->setShaderResourceBindings(mCubeShaderBindings.get()); + mCubePipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mCubePipeline->setDepthTest(true); + mCubePipeline->setDepthWrite(false); + mCubePipeline->setTargetBlends({ blend }); + mCubePipeline->create(); + + mPlanePipeline.reset(mRhi->newGraphicsPipeline()); + mPlanePipeline->setSampleCount(mSwapChain->sampleCount()); + mPlanePipeline->setTopology(QRhiGraphicsPipeline::Triangles); + mPlanePipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + mPlanePipeline->setVertexInputLayout(inputLayout); + mPlanePipeline->setShaderResourceBindings(mCubeShaderBindings.get()); + mPlanePipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mPlanePipeline->setDepthTest(true); + mPlanePipeline->setDepthWrite(true); + mPlanePipeline->setTargetBlends({ blend }); + mPlanePipeline->create(); + + mSigSubmit.request(); + } + + virtual void onRenderTick() override { + if (mSigInit.ensure()) { + initRhiResource(); + } + + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + if (mSigSubmit.ensure()) { + batch->uploadStaticBuffer(mCubeVertexBuffer.get(), CubeData); + batch->uploadStaticBuffer(mPlaneVertexBuffer.get(), PlaneData); + } + + QMatrix4x4 project = mRhi->clipSpaceCorrMatrix(); + + project.perspective(90.0, renderTarget->pixelSize().width() / (float)renderTarget->pixelSize().height(), 0.01, 1000); + QMatrix4x4 view; + view.lookAt(QVector3D(5, 5, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 0)); + + QMatrix4x4 model; + UniformBlock ubo; + ubo.color = QVector4D(0, 0, 1, 0.5); + ubo.MVP = (project * view * model).toGenericMatrix<4, 4>(); + batch->updateDynamicBuffer(mCubeUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); + + ubo.color = QVector4D(1, 0, 0, 1); + ubo.MVP = (project * view * model).toGenericMatrix<4, 4>(); + batch->updateDynamicBuffer(mPlaneUniformBuffer.get(), 0, sizeof(UniformBlock), &ubo); + + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, batch); + + cmdBuffer->setGraphicsPipeline(mPlanePipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(mPlaneShaderBindings.get()); + const QRhiCommandBuffer::VertexInput planeVertexBindings(mPlaneVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &planeVertexBindings); + cmdBuffer->draw(6); + + cmdBuffer->setGraphicsPipeline(mCubePipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(mCubeShaderBindings.get()); + const QRhiCommandBuffer::VertexInput cubeVertexBindings(mCubeVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &cubeVertexBindings); + cmdBuffer->draw(36); + + cmdBuffer->endPass(); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + MyWindow window(initParams); + window.resize({ 800,600 }); + window.show(); + + return app.exec(); +} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/11-ComputeShader/Source/main.cpp b/Source/1-GraphicsAPI/11-ComputeShader/Source/main.cpp deleted file mode 100644 index 2c9c3886..00000000 --- a/Source/1-GraphicsAPI/11-ComputeShader/Source/main.cpp +++ /dev/null @@ -1,115 +0,0 @@ -#include - -#include "Render/RHI/QRhiWindow.h" -#include "Render/Painter/TexturePainter.h" - -class ComputeShaderWindow : public QRhiWindow { -private: - QRhiEx::Signal sigInit; - QRhiEx::Signal sigSubmit; - - QScopedPointer mStorageBuffer; - QScopedPointer mTexture; - - QScopedPointer mPipeline; - QScopedPointer mShaderBindings; - - QScopedPointer mTexturePainter; - - const int ImageWidth = 64; - const int ImageHeight = 64; -public: - ComputeShaderWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) { - sigInit.request(); - sigSubmit.request(); - } -protected: - virtual void onRenderTick() override { - QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - if (sigInit.receive()) { - initRhiResource(); - } - QRhiResourceUpdateBatch* resourceUpdates = nullptr; - if (sigSubmit.receive()) { - resourceUpdates = mRhi->nextResourceUpdateBatch(); - submitRhiData(resourceUpdates); - } - - currentCmdBuffer->beginComputePass(resourceUpdates, QRhiCommandBuffer::BeginPassFlag::ExternalContent); - currentCmdBuffer->setComputePipeline(mPipeline.get()); - currentCmdBuffer->setShaderResources(); - currentCmdBuffer->dispatch(ImageWidth, ImageHeight, 1); - currentCmdBuffer->endComputePass(); - - const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); - const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; - - currentCmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue); - mTexturePainter->paint(currentCmdBuffer, currentRenderTarget); - currentCmdBuffer->endPass(); - } - - void initRhiResource() { - mStorageBuffer.reset(mRhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, sizeof(float))); - mStorageBuffer->create(); - mTexture.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(ImageWidth, ImageHeight), 1, QRhiTexture::UsedWithLoadStore)); - mTexture->create(); - - mTexturePainter.reset(new TexturePainter); - mTexturePainter->setupRhi(mRhi.get()); - mTexturePainter->setupRenderPassDesc(mSwapChain->renderPassDescriptor()); - mTexturePainter->setupSampleCount(mSwapChain->sampleCount()); - mTexturePainter->setupTexture(mTexture.get()); - mTexturePainter->compile(); - - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - mShaderBindings->setBindings({ - QRhiShaderResourceBinding::bufferLoadStore(0,QRhiShaderResourceBinding::ComputeStage,mStorageBuffer.get()), - QRhiShaderResourceBinding::imageLoadStore(1,QRhiShaderResourceBinding::ComputeStage,mTexture.get(),0), - }); - mShaderBindings->create(); - - mPipeline.reset(mRhi->newComputePipeline()); - - QShader cs = mRhi->newShaderFromCode(QShader::ComputeStage, R"(#version 440 -layout(std140, binding = 0) buffer StorageBuffer{ - int counter; -}SSBO; -layout (binding = 1, rgba8) uniform image2D Tex; - -void main(){ - int currentCounter = atomicAdd(SSBO.counter,1); - //int currentCounter = SSBO.counter = SSBO.counter + 1; - ivec2 pos = ivec2(gl_GlobalInvocationID.xy); - imageStore(Tex,pos,vec4(sin(currentCounter/100.0f),0,0,1)); -} -)"); - Q_ASSERT(cs.isValid()); - - mPipeline->setShaderStage({ - QRhiShaderStage(QRhiShaderStage::Compute, cs), - }); - - mPipeline->setShaderResourceBindings(mShaderBindings.get()); - mPipeline->create(); - } - - void submitRhiData(QRhiResourceUpdateBatch* resourceUpdates) { - const int counter = 0; - resourceUpdates->uploadStaticBuffer(mStorageBuffer.get(), &counter); - } -}; - -int main(int argc, char **argv) -{ - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - ComputeShaderWindow window(initParams); - window.resize({ 800,600 }); - window.show(); - app.exec(); - return 0; -} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/11-ComputeShader/README.md b/Source/1-GraphicsAPI/11-FaceCulling/README.md similarity index 100% rename from Source/1-GraphicsAPI/11-ComputeShader/README.md rename to Source/1-GraphicsAPI/11-FaceCulling/README.md diff --git a/Source/1-GraphicsAPI/11-FaceCulling/Source/main.cpp b/Source/1-GraphicsAPI/11-FaceCulling/Source/main.cpp new file mode 100644 index 00000000..2b20cdcc --- /dev/null +++ b/Source/1-GraphicsAPI/11-FaceCulling/Source/main.cpp @@ -0,0 +1,136 @@ +#include +#include +#include "QEngineApplication.h" +#include "Render/RHI/QRhiWindow.h" + +static float VertexData[] = { + //position(xy) color(rgba) + 0.0f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, + -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, + 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f, +}; + +struct UniformBlock{ + QGenericMatrix<4, 4, float> MVP; +}; + +class TriangleWindow : public QRhiWindow { +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + QScopedPointer mVertexBuffer; + QScopedPointer mUniformBuffer; + QScopedPointer mShaderBindings; + QScopedPointer mPipeline; +public: + TriangleWindow(QRhiHelper::InitParams inInitParams) + : QRhiWindow(inInitParams) { + mSigInit.request(); + mSigSubmit.request(); + } +protected: + virtual void onRenderTick() override { + if (mSigInit.ensure()) { //初始化资源 + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + mVertexBuffer->create(); + mUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(UniformBlock))); + mUniformBuffer->create(); + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + QRhiVertexInputBinding(6 * sizeof(float)) + }); + + inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0 , QRhiVertexInputAttribute::Float2, 0), + QRhiVertexInputAttribute(0, 1 , QRhiVertexInputAttribute::Float4, sizeof(float) * 2), + }); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + + mPipeline->setVertexInputLayout(inputLayout); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->setBindings({ + QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::StageFlag::VertexStage, mUniformBuffer.get()), + }); + mShaderBindings->create(); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->setSampleCount(mSwapChain->sampleCount()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; + layout(location = 1) in vec4 color; + layout(location = 0) out vec4 vColor; + layout(binding = 0) uniform UniformBlock{ + mat4 MVP; + }UBO; + + out gl_PerVertex { + vec4 gl_Position; + }; + + void main(){ + gl_Position = UBO.MVP * vec4(position,0.0f,1.0f); + vColor = color; + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout (location = 0) in vec4 vColor; + layout (location = 0) out vec4 fragColor; + void main(){ + fragColor = vColor; + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + QRhiShaderStage(QRhiShaderStage::Vertex, vs), + QRhiShaderStage(QRhiShaderStage::Fragment, fs) + }); + + mPipeline->setFrontFace(QRhiGraphicsPipeline::FrontFace::CCW); //设置逆时针的顶点缠绕方向为正面 + mPipeline->setCullMode(QRhiGraphicsPipeline::CullMode::Back); //开启背面剔除 + mPipeline->create(); + } + + QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + QRhiResourceUpdateBatch* batch = mRhi->nextResourceUpdateBatch(); + if (mSigSubmit.ensure()) { + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + } + QMatrix4x4 MVP = mRhi->clipSpaceCorrMatrix(); + MVP.rotate(QTime::currentTime().msecsSinceStartOfDay() / 5.f, QVector3D(0, 1, 0)); //绕Y轴随时间旋转 + batch->updateDynamicBuffer(mUniformBuffer.get(), 0, sizeof(UniformBlock), MVP.data()); + cmdBuffer->resourceUpdate(batch); + + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue, nullptr); + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexInput(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexInput); + cmdBuffer->draw(3); + + cmdBuffer->endPass(); + } +}; + +int main(int argc, char** argv) +{ + QEngineApplication app(argc, argv); + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + TriangleWindow window(initParams); + window.resize({ 800,600 }); + window.show(); + app.exec(); + return 0; +} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/12-IndirectDraw/Source/main.cpp b/Source/1-GraphicsAPI/12-IndirectDraw/Source/main.cpp deleted file mode 100644 index d2c265e4..00000000 --- a/Source/1-GraphicsAPI/12-IndirectDraw/Source/main.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include - -#include "Render/RHI/QRhiWindow.h" -#include "Render/Painter/TexturePainter.h" -#include "private/qrhivulkan_p.h" -#include "qvulkanfunctions.h" -#include "private/qvulkandefaultinstance_p.h" - -class IndirectDrawWindow : public QRhiWindow { -private: - QRhiEx::Signal sigInit; - QRhiEx::Signal sigSubmit; - - QScopedPointer mStorageBuffer; - QScopedPointer mIndirectDrawBuffer; - - QScopedPointer mPipeline; - QScopedPointer mShaderBindings; - - int mDispatchParam[3] = { 1,1,1 }; -public: - IndirectDrawWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) { - sigInit.request(); - sigSubmit.request(); - } -protected: - void initRhiResource() { - mStorageBuffer.reset(mRhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, sizeof(float))); - mStorageBuffer->create(); - - mIndirectDrawBuffer.reset(mRhi->newVkBuffer(QRhiBuffer::Static, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, sizeof(mDispatchParam))); - mIndirectDrawBuffer->create(); - - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - mShaderBindings->setBindings({ - QRhiShaderResourceBinding::bufferLoadStore(0,QRhiShaderResourceBinding::ComputeStage,mStorageBuffer.get()), - }); - mShaderBindings->create(); - mPipeline.reset(mRhi->newComputePipeline()); - QShader cs = mRhi->newShaderFromCode(QShader::ComputeStage, R"(#version 440 - layout(std140, binding = 0) buffer StorageBuffer{ - int counter; - }SSBO; - layout (binding = 1, rgba8) uniform image2D Tex; - - void main(){ - int currentCounter = atomicAdd(SSBO.counter,1); - } - )"); - Q_ASSERT(cs.isValid()); - - mPipeline->setShaderStage({ - QRhiShaderStage(QRhiShaderStage::Compute, cs), - }); - - mPipeline->setShaderResourceBindings(mShaderBindings.get()); - mPipeline->create(); - } - - virtual void onRenderTick() override { - QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); - QRhiCommandBuffer* currentCmdBuffer = mSwapChain->currentFrameCommandBuffer(); - - if (sigInit.receive()) { - initRhiResource(); - } - QRhiResourceUpdateBatch* resourceUpdates = nullptr; - if(sigSubmit.receive()){ - resourceUpdates = mRhi->nextResourceUpdateBatch(); - resourceUpdates->uploadStaticBuffer(mIndirectDrawBuffer.get(), 0, sizeof(mDispatchParam), &mDispatchParam); - currentCmdBuffer->resourceUpdate(resourceUpdates); - mRhi->finish(); - } - - currentCmdBuffer->beginComputePass(nullptr, QRhiCommandBuffer::ExternalContent); - currentCmdBuffer->setComputePipeline(mPipeline.get()); - currentCmdBuffer->setShaderResources(); - - QRhiVulkanCommandBufferNativeHandles* vkCmdBufferHandle = (QRhiVulkanCommandBufferNativeHandles*)currentCmdBuffer->nativeHandles(); - QRhiVulkanNativeHandles* vkHandles = (QRhiVulkanNativeHandles*)mRhi->nativeHandles(); - auto buffer = mIndirectDrawBuffer->nativeBuffer(); - VkBuffer vkBuffer = *(VkBuffer*)buffer.objects[0]; - QVulkanInstance* vkInstance = QVulkanDefaultInstance::instance(); - vkInstance->deviceFunctions(vkHandles->dev)->vkCmdDispatchIndirect(vkCmdBufferHandle->commandBuffer, vkBuffer, 0); - currentCmdBuffer->endComputePass(); - static QRhiBufferReadbackResult mCtxReader; - mCtxReader.completed = [this]() { - int counter; - memcpy(&counter, mCtxReader.data.constData(), mCtxReader.data.size()); - qDebug() << counter; - }; - resourceUpdates = mRhi->nextResourceUpdateBatch(); - resourceUpdates->readBackBuffer(mStorageBuffer.get(), 0, sizeof(float), &mCtxReader); - currentCmdBuffer->resourceUpdate(resourceUpdates); - mRhi->finish(); - mDispatchParam[0]++; - } -}; - - -int main(int argc, char **argv) -{ - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Vulkan; - IndirectDrawWindow window(initParams); - window.resize({ 800,600 }); - window.show(); - app.exec(); - return 0; -} \ No newline at end of file diff --git a/Source/1-GraphicsAPI/12-IndirectDraw/README.md b/Source/1-GraphicsAPI/12-Instancing/README.md similarity index 100% rename from Source/1-GraphicsAPI/12-IndirectDraw/README.md rename to Source/1-GraphicsAPI/12-Instancing/README.md diff --git a/Source/1-GraphicsAPI/12-Instancing/Source/main.cpp b/Source/1-GraphicsAPI/12-Instancing/Source/main.cpp new file mode 100644 index 00000000..e7a7164d --- /dev/null +++ b/Source/1-GraphicsAPI/12-Instancing/Source/main.cpp @@ -0,0 +1,137 @@ +#include +#include "Render/RHI/QRhiWindow.h" + +static float VertexData[] = { + //position (xy) + 0.0f, 0.02f, + -0.02f, -0.02f, + 0.02f, -0.02f, +}; + +class InstancingWindow : public QRhiWindow { +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mVertexBuffer; + QScopedPointer mInstancingBuffer; + QScopedPointer mShaderBindings; + QScopedPointer mPipeline; + QVector mInstanceData; +public: + InstancingWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + mSigSubmit.request(); + + float offset = 0.05f; + for (float i = -1.0f; i <= 1.0f; i += offset) { + for (float j = -1.0f; j <= 1.0f; j += offset) { + mInstanceData.push_back({ i,j }); + } + } + } +protected: + virtual void onRenderTick() override { + QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + if (mSigInit.ensure()) { + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + mVertexBuffer->create(); + + mInstancingBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(QVector2D) * mInstanceData.size())); + mInstancingBuffer->create(); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->create(); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + + QRhiGraphicsPipeline::TargetBlend targetBlend; + targetBlend.enable = false; + mPipeline->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); + + mPipeline->setSampleCount(mSwapChain->sampleCount()); + + mPipeline->setDepthTest(false); + mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); + mPipeline->setDepthWrite(false); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; + layout(location = 1) in vec2 offset; + + out gl_PerVertex { + vec4 gl_Position; + }; + void main(){ + gl_Position = vec4(position + offset,0.0f,1.0f); + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(location = 0) out vec4 fragColor; + void main(){ + fragColor = vec4(0.1f,0.5f,0.9f,1.0f); + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + QRhiVertexInputBinding(2 * sizeof(float)), + QRhiVertexInputBinding(2 * sizeof(float), QRhiVertexInputBinding::PerInstance), //ʵ + }); + inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0), + QRhiVertexInputAttribute(1, 1, QRhiVertexInputAttribute::Float2, 0), + }); + + mPipeline->setVertexInputLayout(inputLayout); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->setRenderPassDescriptor(mSwapChainPassDesc.get()); + mPipeline->create(); + } + QRhiResourceUpdateBatch* resourceUpdates = nullptr; + if (mSigSubmit.ensure()) { + resourceUpdates = mRhi->nextResourceUpdateBatch(); + resourceUpdates->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + resourceUpdates->uploadStaticBuffer(mInstancingBuffer.get(), mInstanceData.data()); + } + const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue, resourceUpdates); + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mSwapChain->currentPixelSize().width(), mSwapChain->currentPixelSize().height())); + cmdBuffer->setShaderResources(); + + const QRhiCommandBuffer::VertexInput vertexBindings[] = { + { mVertexBuffer.get(), 0 }, + { mInstancingBuffer.get(), 0 }, + }; + cmdBuffer->setVertexInput(0, 2, vertexBindings); + cmdBuffer->draw(3, mInstanceData.size()); + cmdBuffer->endPass(); + } +}; +int main(int argc, char **argv) +{ + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::D3D11; + InstancingWindow* window = new InstancingWindow(initParams); + window->resize({ 800,600 }); + window->show(); + app.exec(); + delete window; + return 0; +} \ No newline at end of file diff --git a/Source/2-EngineTechnology/01-Editor/README.md b/Source/1-GraphicsAPI/13-MultiRenderTarget/README.md similarity index 100% rename from Source/2-EngineTechnology/01-Editor/README.md rename to Source/1-GraphicsAPI/13-MultiRenderTarget/README.md diff --git a/Source/1-GraphicsAPI/13-MultiRenderTarget/Source/main.cpp b/Source/1-GraphicsAPI/13-MultiRenderTarget/Source/main.cpp new file mode 100644 index 00000000..ef57eb1e --- /dev/null +++ b/Source/1-GraphicsAPI/13-MultiRenderTarget/Source/main.cpp @@ -0,0 +1,233 @@ +#include + +#include "Render/RHI/QRhiWindow.h" +#include "Render/RenderGraph/Painter/TexturePainter.h" + +static float VertexData[] = { + //position(xy) + 0.0f, 0.5f, + -0.5f, -0.5f, + 0.5f, -0.5f, +}; + +class MRTWindow : public QRhiWindow { +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mVertexBuffer; + QScopedPointer mShaderBindings; + QScopedPointer mPipeline; + + QScopedPointer mColorAttachment0; + QScopedPointer mColorAttachment1; + QScopedPointer mDepthStencilBuffer; + + QScopedPointer mRenderTarget; + QScopedPointer mRenderPassDesc; + + QScopedPointer mPaintSampler; + QScopedPointer mPaintShaderBindings; + QScopedPointer mPaintPipeline; +public: + MRTWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + mSigSubmit.request(); + } +protected: + virtual void onRenderTick() override { + QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + if (mSigInit.ensure()) { + mColorAttachment0.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(100, 100), 1, QRhiTexture::Flag::RenderTarget | QRhiTexture::UsedAsTransferSource)); //ɫ0 + mColorAttachment0->create(); + mColorAttachment1.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(100, 100), 1, QRhiTexture::Flag::RenderTarget | QRhiTexture::UsedAsTransferSource)); //ɫ1 + mColorAttachment1->create(); + mDepthStencilBuffer.reset(mRhi->newRenderBuffer(QRhiRenderBuffer::Type::DepthStencil, QSize(100, 100), 1, QRhiRenderBuffer::Flag(), QRhiTexture::Format::D24S8)); //ȣ24λģ棨8λ + mDepthStencilBuffer->create(); + + QRhiTextureRenderTargetDescription rtDesc; + rtDesc.setColorAttachments({ mColorAttachment0.get(),mColorAttachment1.get() }); + rtDesc.setDepthStencilBuffer(mDepthStencilBuffer.get()); + mRenderTarget.reset(mRhi->newTextureRenderTarget(rtDesc)); + + //RenderTargetĽṹRenderPassʹGraphicsPipelineʱָRenderPassDesc + //ΪˮڴʱҪȷںֽṹRenderPassĽṹָǣRenderTargetĸ͸ʽ + mRenderPassDesc.reset(mRenderTarget->newCompatibleRenderPassDescriptor()); + mRenderTarget->setRenderPassDescriptor(mRenderPassDesc.get()); + + mRenderTarget->create(); + + mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + mVertexBuffer->create(); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->create(); + + mPipeline.reset(mRhi->newGraphicsPipeline()); + + QRhiGraphicsPipeline::TargetBlend targetBlend; + targetBlend.enable = false; + mPipeline->setTargetBlends({ targetBlend,targetBlend }); + + mPipeline->setSampleCount(mRenderTarget->sampleCount()); + + mPipeline->setDepthTest(false); + mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); + mPipeline->setDepthWrite(false); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; + out gl_PerVertex { + vec4 gl_Position; + }; + void main(){ + gl_Position = vec4(position,0.0f,1.0f); + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(location = 0) out vec4 fragColor0; //ɫ0 + layout(location = 1) out vec4 fragColor1; //ɫ1 + void main(){ + fragColor0 = vec4(1.0f,0.0f,0.0f,1.0f); + fragColor1 = vec4(0.0f,0.0f,1.0f,1.0f); + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + QRhiShaderStage(QRhiShaderStage::Vertex, vs), + QRhiShaderStage(QRhiShaderStage::Fragment, fs) + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + QRhiVertexInputBinding(2 * sizeof(float)) + }); + + inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0), + }); + + mPipeline->setVertexInputLayout(inputLayout); + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->setRenderPassDescriptor(mRenderPassDesc.get()); + mPipeline->create(); + + mPaintPipeline.reset(mRhi->newGraphicsPipeline()); + QRhiGraphicsPipeline::TargetBlend blendState; + blendState.dstColor = QRhiGraphicsPipeline::One; + blendState.srcColor = QRhiGraphicsPipeline::One; + blendState.dstAlpha = QRhiGraphicsPipeline::One; + blendState.srcAlpha = QRhiGraphicsPipeline::One; + blendState.enable = true; + mPaintPipeline->setTargetBlends({ blendState }); + mPaintPipeline->setSampleCount(currentRenderTarget->sampleCount()); + mPaintPipeline->setDepthTest(false); + + QString vsCode = R"(#version 450 + layout (location = 0) out vec2 vUV; + out gl_PerVertex{ + vec4 gl_Position; + }; + void main() { + vUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(vUV * 2.0f - 1.0f, 0.0f, 1.0f); + %1 + } + )"; + vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, vsCode.arg(mRhi->isYUpInNDC() ? " vUV.y = 1 - vUV.y;" : "").toLocal8Bit()); + + fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 450 + layout (binding = 0) uniform sampler2D uSamplerColor; + layout (location = 0) in vec2 vUV; + layout (location = 0) out vec4 outFragColor; + void main() { + outFragColor = vec4(texture(uSamplerColor, vUV).rgb,1.0f); + } + )"); + mPaintPipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + mPaintSampler.reset(mRhi->newSampler( + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::None, + QRhiSampler::Repeat, + QRhiSampler::Repeat, + QRhiSampler::Repeat + )); + mPaintSampler->create(); + + mPaintShaderBindings.reset(mRhi->newShaderResourceBindings()); + mPaintShaderBindings->setBindings({ + QRhiShaderResourceBinding::sampledTexture(0,QRhiShaderResourceBinding::FragmentStage,mColorAttachment0.get(),mPaintSampler.get()) + }); + mPaintShaderBindings->create(); + mPaintPipeline->setShaderResourceBindings(mPaintShaderBindings.get()); + mPaintPipeline->setRenderPassDescriptor(currentRenderTarget->renderPassDescriptor()); + mPaintPipeline->create(); + } + + QRhiResourceUpdateBatch* resourceUpdates = nullptr; + if (mSigSubmit.ensure()) { + resourceUpdates = mRhi->nextResourceUpdateBatch(); + resourceUpdates->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + } + + const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(mRenderTarget.get(), clearColor, dsClearValue, resourceUpdates); //PassжɫȾĿ + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mRenderTarget->pixelSize().width(), mRenderTarget->pixelSize().height())); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexBindings); + cmdBuffer->draw(3); + + cmdBuffer->endPass(); + + static int counter = 0; + static QRhiTexture* CurrentTexture = nullptr; //ÿ60֡潫ɫƵ + if (counter && counter % 60 == 0) { + if (!CurrentTexture || CurrentTexture == mColorAttachment1.get()) + CurrentTexture = mColorAttachment0.get(); + else { + CurrentTexture = mColorAttachment1.get(); + } + mPaintShaderBindings->setBindings({ + QRhiShaderResourceBinding::sampledTexture(0,QRhiShaderResourceBinding::FragmentStage,CurrentTexture,mPaintSampler.get()) // + }); + mPaintShaderBindings->create(); + counter = 0; + } + counter++; + + cmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue); + + cmdBuffer->setGraphicsPipeline(mPaintPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, currentRenderTarget->pixelSize().width(), currentRenderTarget->pixelSize().height())); + cmdBuffer->setShaderResources(mPaintShaderBindings.get()); + cmdBuffer->draw(4); + + cmdBuffer->endPass(); + } +}; + +int main(int argc, char **argv) +{ + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + QRhiHelper::InitParams initParams; + MRTWindow window(initParams); + window.resize({ 800,600 }); + window.show(); + app.exec(); + return 0; +} \ No newline at end of file diff --git a/Source/2-EngineTechnology/02-ImGUI/README.md b/Source/1-GraphicsAPI/14-ComputePipeline/README.md similarity index 100% rename from Source/2-EngineTechnology/02-ImGUI/README.md rename to Source/1-GraphicsAPI/14-ComputePipeline/README.md diff --git a/Source/1-GraphicsAPI/14-ComputePipeline/Source/main.cpp b/Source/1-GraphicsAPI/14-ComputePipeline/Source/main.cpp new file mode 100644 index 00000000..6b368545 --- /dev/null +++ b/Source/1-GraphicsAPI/14-ComputePipeline/Source/main.cpp @@ -0,0 +1,156 @@ +#include +#include "Render/RHI/QRhiWindow.h" + +class ComputeShaderWindow : public QRhiWindow { +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mStorageBuffer; + QScopedPointer mTexture; + + QScopedPointer mPipeline; + QScopedPointer mShaderBindings; + + QScopedPointer mPaintSampler; + QScopedPointer mPaintShaderBindings; + QScopedPointer mPaintPipeline; + + const int ImageWidth = 64; + const int ImageHeight = 64; +public: + ComputeShaderWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + mSigSubmit.request(); + } +protected: + virtual void onRenderTick() override { + QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + if (mSigInit.ensure()) { + mStorageBuffer.reset(mRhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, sizeof(float))); //StorageBuffer + mStorageBuffer->create(); + mTexture.reset(mRhi->newTexture(QRhiTexture::RGBA8, QSize(ImageWidth, ImageHeight), 1, QRhiTexture::UsedWithLoadStore)); //ͼɱ߶ȡʹ洢 + mTexture->create(); + + mPipeline.reset(mRhi->newComputePipeline()); + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->setBindings({ + QRhiShaderResourceBinding::bufferLoadStore(0,QRhiShaderResourceBinding::ComputeStage,mStorageBuffer.get()), //üߵԴ󶨣LoadɶStoreд + QRhiShaderResourceBinding::imageLoadStore(1,QRhiShaderResourceBinding::ComputeStage,mTexture.get(),0), + }); + mShaderBindings->create(); + + QShader cs = QRhiHelper::newShaderFromCode(QShader::ComputeStage, R"(#version 440 + layout(std140, binding = 0) buffer StorageBuffer{ + int counter; + }SSBO; + layout (binding = 1, rgba8) uniform image2D Tex; + const int ImageSize = 64 * 64; + void main(){ + //int currentCounter = SSBO.counter = SSBO.counter + 1; + int currentCounter = atomicAdd(SSBO.counter,1); //use atomic operation + ivec2 pos = ivec2(gl_GlobalInvocationID.xy); + imageStore(Tex,pos,vec4(currentCounter/float(ImageSize),0,0,1)); + } + )"); + Q_ASSERT(cs.isValid()); + + mPipeline->setShaderStage({ + QRhiShaderStage(QRhiShaderStage::Compute, cs), + }); + + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->create(); + + mPaintPipeline.reset(mRhi->newGraphicsPipeline()); + QRhiGraphicsPipeline::TargetBlend blendState; + blendState.dstColor = QRhiGraphicsPipeline::One; + blendState.srcColor = QRhiGraphicsPipeline::One; + blendState.dstAlpha = QRhiGraphicsPipeline::One; + blendState.srcAlpha = QRhiGraphicsPipeline::One; + blendState.enable = true; + mPaintPipeline->setTargetBlends({ blendState }); + mPaintPipeline->setSampleCount(currentRenderTarget->sampleCount()); + mPaintPipeline->setDepthTest(false); + + QString vsCode = R"(#version 450 + layout (location = 0) out vec2 vUV; + out gl_PerVertex{ + vec4 gl_Position; + }; + void main() { + vUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(vUV * 2.0f - 1.0f, 0.0f, 1.0f); + %1 + } + )"; + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, vsCode.arg(mRhi->isYUpInNDC() ? " vUV.y = 1 - vUV.y;" : "").toLocal8Bit()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 450 + layout (binding = 0) uniform sampler2D uSamplerColor; + layout (location = 0) in vec2 vUV; + layout (location = 0) out vec4 outFragColor; + void main() { + outFragColor = vec4(texture(uSamplerColor, vUV).rgb,1.0f); + } + )"); + mPaintPipeline->setShaderStages({ + { QRhiShaderStage::Vertex, vs }, + { QRhiShaderStage::Fragment, fs } + }); + + mPaintSampler.reset(mRhi->newSampler( + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::Linear, + QRhiSampler::Filter::None, + QRhiSampler::Repeat, + QRhiSampler::Repeat, + QRhiSampler::Repeat + )); + mPaintSampler->create(); + + mPaintShaderBindings.reset(mRhi->newShaderResourceBindings()); + mPaintShaderBindings->setBindings({ + QRhiShaderResourceBinding::sampledTexture(0,QRhiShaderResourceBinding::FragmentStage,mTexture.get(),mPaintSampler.get()) + }); + mPaintShaderBindings->create(); + mPaintPipeline->setShaderResourceBindings(mPaintShaderBindings.get()); + mPaintPipeline->setRenderPassDescriptor(currentRenderTarget->renderPassDescriptor()); + mPaintPipeline->create(); + } + + QRhiResourceUpdateBatch* resourceUpdates = nullptr; + resourceUpdates = mRhi->nextResourceUpdateBatch(); + const int counter = 0; + resourceUpdates->uploadStaticBuffer(mStorageBuffer.get(), &counter); + cmdBuffer->beginComputePass(resourceUpdates); + cmdBuffer->setComputePipeline(mPipeline.get()); + cmdBuffer->setShaderResources(); + cmdBuffer->dispatch(ImageWidth, ImageHeight, 1); //ͼСֹ + cmdBuffer->endComputePass(); + + const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(currentRenderTarget, clearColor, dsClearValue); + cmdBuffer->setGraphicsPipeline(mPaintPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, currentRenderTarget->pixelSize().width(), currentRenderTarget->pixelSize().height())); + cmdBuffer->setShaderResources(mPaintShaderBindings.get()); + cmdBuffer->draw(4); + cmdBuffer->endPass(); + } +}; + +int main(int argc, char **argv) +{ + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + QRhiHelper::InitParams initParams; + ComputeShaderWindow window(initParams); + window.resize({ 800,600 }); + window.show(); + app.exec(); + return 0; +} \ No newline at end of file diff --git a/Source/2-EngineTechnology/03-DebugDraw/README.md b/Source/1-GraphicsAPI/15-IndirectDraw/README.md similarity index 100% rename from Source/2-EngineTechnology/03-DebugDraw/README.md rename to Source/1-GraphicsAPI/15-IndirectDraw/README.md diff --git a/Source/1-GraphicsAPI/15-IndirectDraw/Source/main.cpp b/Source/1-GraphicsAPI/15-IndirectDraw/Source/main.cpp new file mode 100644 index 00000000..cae12b3e --- /dev/null +++ b/Source/1-GraphicsAPI/15-IndirectDraw/Source/main.cpp @@ -0,0 +1,119 @@ +#include + +#include "Render/RHI/QRhiWindow.h" +#include "private/qrhivulkan_p.h" +#include "qvulkanfunctions.h" + +struct DispatchStruct { + int x, y, z; +}; + +class IndirectDrawWindow : public QRhiWindow { +private: + QRhiSignal mSigInit; + QRhiSignal mSigSubmit; + + QScopedPointer mStorageBuffer; + QScopedPointer mIndirectDrawBuffer; + + QScopedPointer mPipeline; + QScopedPointer mShaderBindings; +public: + IndirectDrawWindow(QRhiHelper::InitParams inInitParams) :QRhiWindow(inInitParams) { + mSigInit.request(); + mSigSubmit.request(); + } +protected: + virtual void onRenderTick() override { + QRhiRenderTarget* currentRenderTarget = mSwapChain->currentFrameRenderTarget(); + QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer(); + + if (mSigInit.ensure()) { + mStorageBuffer.reset(mRhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, sizeof(float))); + mStorageBuffer->create(); + + mIndirectDrawBuffer.reset(QRhiHelper::newVkBuffer(mRhi.get(), QRhiBuffer::Static, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, sizeof(DispatchStruct))); + mIndirectDrawBuffer->create(); + + mShaderBindings.reset(mRhi->newShaderResourceBindings()); + mShaderBindings->setBindings({ + QRhiShaderResourceBinding::bufferLoadStore(0,QRhiShaderResourceBinding::ComputeStage,mStorageBuffer.get()), + }); + mShaderBindings->create(); + mPipeline.reset(mRhi->newComputePipeline()); + QShader cs = QRhiHelper::newShaderFromCode(QShader::ComputeStage, R"(#version 440 + layout(std140, binding = 0) buffer StorageBuffer{ + int counter; + }SSBO; + layout (binding = 1, rgba8) uniform image2D Tex; + + void main(){ + int currentCounter = atomicAdd(SSBO.counter,1); + } + )"); + Q_ASSERT(cs.isValid()); + + mPipeline->setShaderStage({ + QRhiShaderStage(QRhiShaderStage::Compute, cs), + }); + + mPipeline->setShaderResourceBindings(mShaderBindings.get()); + mPipeline->create(); + } + QRhiResourceUpdateBatch* resourceUpdates = nullptr; + if(mSigSubmit.ensure()){ + resourceUpdates = mRhi->nextResourceUpdateBatch(); //ʼӻ + DispatchStruct dispatch; + dispatch.x = dispatch.y = dispatch.z = 1; + resourceUpdates->uploadStaticBuffer(mIndirectDrawBuffer.get(), 0, sizeof(DispatchStruct), &dispatch); + cmdBuffer->resourceUpdate(resourceUpdates); + mRhi->finish(); + } + + QVulkanInstance* vkInstance = vulkanInstance(); + QRhiVulkanNativeHandles* vkHandle = (QRhiVulkanNativeHandles*)mRhi->nativeHandles(); + QVulkanDeviceFunctions* vkDevFunc = vkInstance->deviceFunctions(vkHandle->dev); + + QRhiVulkanCommandBufferNativeHandles* cmdBufferHandles = (QRhiVulkanCommandBufferNativeHandles*)cmdBuffer->nativeHandles(); + QVkCommandBuffer* cbD = QRHI_RES(QVkCommandBuffer, cmdBuffer); + VkCommandBuffer vkCmdBuffer = cmdBufferHandles->commandBuffer; + + QVkComputePipeline* pipelineHandle = (QVkComputePipeline*)mPipeline.get(); + VkPipeline vkPipeline = pipelineHandle->pipeline; + + QRhiBuffer::NativeBuffer indirectBufferHandle = mIndirectDrawBuffer->nativeBuffer(); + VkBuffer vkIndirectBuffer = *(VkBuffer*)indirectBufferHandle.objects[0]; + + cmdBuffer->beginExternal(); //ʼչ֮ԭAPIָ + vkDevFunc->vkCmdBindPipeline(vkCmdBuffer, VkPipelineBindPoint::VK_PIPELINE_BIND_POINT_COMPUTE, vkPipeline); + QRhiHelper::setShaderResources(mPipeline.get(), cmdBuffer, mShaderBindings.get()); //ڸVKˮߵ + vkDevFunc->vkCmdDispatchIndirect(vkCmdBuffer, vkIndirectBuffer, 0); + + cmdBuffer->endExternal(); + + static QRhiReadbackResult mCtxReader; + mCtxReader.completed = [this]() { + int counter; + memcpy(&counter, mCtxReader.data.constData(), mCtxReader.data.size()); + qDebug() << counter; + }; + resourceUpdates = mRhi->nextResourceUpdateBatch(); + resourceUpdates->readBackBuffer(mStorageBuffer.get(), 0, sizeof(float), &mCtxReader); + cmdBuffer->resourceUpdate(resourceUpdates); + mRhi->finish(); //ȴض + } +}; + + +int main(int argc, char **argv) +{ + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Vulkan; + IndirectDrawWindow window(initParams); + window.resize({ 800,600 }); + window.show(); + app.exec(); + return 0; +} \ No newline at end of file diff --git a/Source/2-EngineTechnology/04-DigitalSignalProcessing/README.md b/Source/1-GraphicsAPI/16-Offscreen/README.md similarity index 100% rename from Source/2-EngineTechnology/04-DigitalSignalProcessing/README.md rename to Source/1-GraphicsAPI/16-Offscreen/README.md diff --git a/Source/1-GraphicsAPI/10-Offscreen/Source/main.cpp b/Source/1-GraphicsAPI/16-Offscreen/Source/main.cpp similarity index 92% rename from Source/1-GraphicsAPI/10-Offscreen/Source/main.cpp rename to Source/1-GraphicsAPI/16-Offscreen/Source/main.cpp index c62659bd..65ebf087 100644 --- a/Source/1-GraphicsAPI/10-Offscreen/Source/main.cpp +++ b/Source/1-GraphicsAPI/16-Offscreen/Source/main.cpp @@ -1,7 +1,7 @@ -#include +#include #include #include -#include "Render/RHI/QRhiEx.h" +#include "Render/RHI/QRhiHelper.h" static float VertexData[] = { //position(xy) @@ -15,7 +15,7 @@ int main(int argc, char **argv) qputenv("QSG_INFO", "1"); QApplication app(argc, argv); - QSharedPointer rhi = QRhiEx::newRhiEx(); + QSharedPointer rhi = QRhiHelper::create(); QScopedPointer renderTargetTexture; QScopedPointer renderTarget; QScopedPointer renderTargetDesc; @@ -53,7 +53,7 @@ int main(int argc, char **argv) mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); mPipeline->setDepthWrite(false); - QShader vs = rhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 layout(location = 0) in vec2 position; out gl_PerVertex { vec4 gl_Position; @@ -64,7 +64,7 @@ int main(int argc, char **argv) )"); Q_ASSERT(vs.isValid()); - QShader fs = rhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 layout(location = 0) out vec4 fragColor; void main(){ fragColor = vec4(0.1f,0.5f,0.9f,1.0f); @@ -113,7 +113,7 @@ int main(int argc, char **argv) resourceUpdates = rhi->nextResourceUpdateBatch(); QRhiReadbackResult rbResult; - QString outputPath = "output.png"; + QString outputPath = "offscreen.png"; rbResult.completed = [&rbResult, &rhi, &outputPath] { if (!rbResult.data.isEmpty()) { const uchar* p = reinterpret_cast(rbResult.data.constData()); diff --git a/Source/2-EngineTechnology/05-VideoRendering/README.md b/Source/1-GraphicsAPI/17-MSAA/README.md similarity index 100% rename from Source/2-EngineTechnology/05-VideoRendering/README.md rename to Source/1-GraphicsAPI/17-MSAA/README.md diff --git a/Source/1-GraphicsAPI/17-MSAA/Source/main.cpp b/Source/1-GraphicsAPI/17-MSAA/Source/main.cpp new file mode 100644 index 00000000..511cd675 --- /dev/null +++ b/Source/1-GraphicsAPI/17-MSAA/Source/main.cpp @@ -0,0 +1,142 @@ +#include +#include +#include +#include "Render/RHI/QRhiHelper.h" + +static float VertexData[] = { + //position(xy) + 0.0f, 0.5f, + -0.5f, -0.5f, + 0.5f, -0.5f, +}; + +int main(int argc, char **argv) +{ + qputenv("QSG_INFO", "1"); + QApplication app(argc, argv); + + QSharedPointer rhi = QRhiHelper::create(); + + if (!rhi->isFeatureSupported(QRhi::Feature::MultisampleTexture)) { + return -1; + } + + QScopedPointer msaaTexture; + QScopedPointer msaaResolveTexture; + QScopedPointer renderTargetTexture; + QScopedPointer renderTarget; + QScopedPointer renderTargetDesc; + + msaaTexture.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(1280, 720), 8, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); + msaaTexture->create(); + msaaResolveTexture.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(1280, 720), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); + msaaResolveTexture->create(); + QRhiColorAttachment colorAttachment; + colorAttachment.setTexture(msaaTexture.get()); + colorAttachment.setResolveTexture(msaaResolveTexture.get()); + renderTarget.reset(rhi->newTextureRenderTarget({ colorAttachment })); + renderTargetDesc.reset(renderTarget->newCompatibleRenderPassDescriptor()); + renderTarget->setRenderPassDescriptor(renderTargetDesc.get()); + renderTarget->create(); + + QScopedPointer vertexBuffer; + QScopedPointer shaderBindings; + QScopedPointer mPipeline; + + { + vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); + vertexBuffer->create(); + + shaderBindings.reset(rhi->newShaderResourceBindings()); + shaderBindings->create(); + + mPipeline.reset(rhi->newGraphicsPipeline()); + + QRhiGraphicsPipeline::TargetBlend targetBlend; + targetBlend.enable = false; + mPipeline->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); + + mPipeline->setSampleCount(renderTarget->sampleCount()); + + mPipeline->setDepthTest(false); + mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); + mPipeline->setDepthWrite(false); + + QShader vs = QRhiHelper::newShaderFromCode(QShader::VertexStage, R"(#version 440 + layout(location = 0) in vec2 position; + out gl_PerVertex { + vec4 gl_Position; + }; + void main(){ + gl_Position = vec4(position,0.0f,1.0f); + } + )"); + Q_ASSERT(vs.isValid()); + + QShader fs = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 440 + layout(location = 0) out vec4 fragColor; + void main(){ + fragColor = vec4(0.1f,0.5f,0.9f,1.0f); + } + )"); + Q_ASSERT(fs.isValid()); + + mPipeline->setShaderStages({ + QRhiShaderStage(QRhiShaderStage::Vertex, vs), + QRhiShaderStage(QRhiShaderStage::Fragment, fs) + }); + + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + QRhiVertexInputBinding(2 * sizeof(float)) + }); + + inputLayout.setAttributes({ + QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0), + }); + + mPipeline->setVertexInputLayout(inputLayout); + mPipeline->setShaderResourceBindings(shaderBindings.get()); + mPipeline->setRenderPassDescriptor(renderTargetDesc.get()); + mPipeline->create(); + } + + QRhiCommandBuffer* cmdBuffer; + if (rhi->beginOffscreenFrame(&cmdBuffer) != QRhi::FrameOpSuccess) + return 1; + + QRhiResourceUpdateBatch* resourceUpdates = rhi->nextResourceUpdateBatch(); + resourceUpdates->uploadStaticBuffer(vertexBuffer.get(), VertexData); + + const QColor clearColor = QColor::fromRgbF(0.2f, 0.2f, 0.2f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + + cmdBuffer->beginPass(renderTarget.get(), clearColor, dsClearValue, resourceUpdates); + + cmdBuffer->setGraphicsPipeline(mPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, msaaTexture->pixelSize().width(), msaaTexture->pixelSize().height())); + cmdBuffer->setShaderResources(); + const QRhiCommandBuffer::VertexInput vertexBindings(vertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexBindings); + cmdBuffer->draw(3); + + resourceUpdates = rhi->nextResourceUpdateBatch(); + QRhiReadbackResult rbResult; + QString outputPath = "msaa.png"; + rbResult.completed = [&rbResult, &rhi, &outputPath] { + if (!rbResult.data.isEmpty()) { + const uchar* p = reinterpret_cast(rbResult.data.constData()); + QImage image(p, rbResult.pixelSize.width(), rbResult.pixelSize.height(), QImage::Format_RGBA8888); + if (rhi->isYUpInFramebuffer()) + image.mirrored().save(outputPath); + else + image.save(outputPath); + } + }; + QRhiReadbackDescription rb(msaaResolveTexture.get()); //回读 msaaResolveTexture 而不是 msaaTexture + resourceUpdates->readBackTexture(rb, &rbResult); + cmdBuffer->endPass(resourceUpdates); + rhi->endOffscreenFrame(); + QDesktopServices::openUrl(QUrl("file:" + outputPath, QUrl::TolerantMode)); + return app.exec(); +} \ No newline at end of file diff --git a/Source/2-EngineTechnology/00-RenderingArchitecture/Source/main.cpp b/Source/2-EngineTechnology/00-RenderingArchitecture/Source/main.cpp index 762368da..ece4b553 100644 --- a/Source/2-EngineTechnology/00-RenderingArchitecture/Source/main.cpp +++ b/Source/2-EngineTechnology/00-RenderingArchitecture/Source/main.cpp @@ -1,7 +1,8 @@ -#include +#include "QEngineApplication.h" #include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" #include "Render/IRenderComponent.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" static float VertexData[] = { //position(xy) @@ -10,104 +11,163 @@ static float VertexData[] = { 0.5f, 0.5f, }; -class QTriangleRenderComponent : public IRenderComponent { +class QTriangleRenderComponent: public IRenderComponent { + Q_OBJECT + Q_PROPERTY(QColor Color READ getColor WRITE setColor) +public: + QColor getColor() const { return mColor; } + void setColor(QColor val) { mColor = val; } +private: QScopedPointer mVertexBuffer; - QScopedPointer mShaderBindings; - QScopedPointer mPipeline; - - Q_BUILDER_BEGIN(QTriangleRenderComponent) - Q_BUILDER_END() + QSharedPointer mProxy; + QColor mColor = QColor::fromRgbF(0.1f, 0.5f, 0.9f, 1.0f); protected: void onRebuildResource() override { mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); mVertexBuffer->create(); - } - void onRebuildPipeline() override { - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - mShaderBindings->create(); - mPipeline.reset(mRhi->newGraphicsPipeline()); + mProxy = newPrimitiveRenderProxy(); - QRhiGraphicsPipeline::TargetBlend targetBlend; - targetBlend.enable = false; - mPipeline->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); + mProxy->addUniformBlock(QRhiShaderStage::Fragment, "UBO") + ->addParam("Color", mColor); - mPipeline->setSampleCount(getBasePass()->getSampleCount()); + mProxy->setInputBindings({ + QRhiVertexInputBindingEx(mVertexBuffer.get(),2 * sizeof(float)) + }); - mPipeline->setDepthTest(false); - mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); - mPipeline->setDepthWrite(false); - - QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 - layout(location = 0) in vec2 position; - out gl_PerVertex { - vec4 gl_Position; - }; + mProxy->setInputAttribute({ + QRhiVertexInputAttributeEx("inPosition",0, 0, QRhiVertexInputAttribute::Float2, 0), + }); + mProxy->setShaderMainCode(QRhiShaderStage::Vertex, R"( void main(){ - gl_Position = vec4(position,0.0f,1.0f); + gl_Position =vec4(inPosition, 0.0f,1.0f); } )"); - Q_ASSERT(vs.isValid()); - - QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 - layout(location = 0) out vec4 fragColor; + mProxy->setShaderMainCode(QRhiShaderStage::Fragment, QString(R"( void main(){ - fragColor = vec4(0.1f,0.5f,0.9f,1.0f); + %1 + })") + .arg(hasColorAttachment("BaseColor")? "BaseColor = UBO.Color;" : "") + .toLocal8Bit() + ); + mProxy->setOnUpload([this](QRhiResourceUpdateBatch* batch) { + batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); + }); + mProxy->setOnUpdate([this](QRhiResourceUpdateBatch* batch, const QPrimitiveRenderProxy::UniformBlocks& blocks, const QPrimitiveRenderProxy::UpdateContext& ctx) { + blocks["UBO"]->setParamValue("Color", QVariant::fromValue(mColor)); + }); + mProxy->setOnDraw([this](QRhiCommandBuffer* cmdBuffer) { + const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); + cmdBuffer->setVertexInput(0, 1, &vertexBindings); + cmdBuffer->draw(3); + }); + } +}; + +class QOutliningPassBuilder : public IRenderPassBuilder { + QRP_INPUT_BEGIN(QOutliningPassBuilder) + QRP_INPUT_ATTR(QRhiTextureRef, BaseColor); + QRP_INPUT_END() + + QRP_OUTPUT_BEGIN(QOutliningPassBuilder) + QRP_OUTPUT_ATTR(QRhiTextureRef, OutliningResult) + QRP_OUTPUT_END() +private: + QRhiTextureRef mColorAttachment; + QRhiTextureRenderTargetRef mRenderTarget; + QShader mOutliningFS; + QRhiSamplerRef mSampler; + QRhiShaderResourceBindingsRef mOutliningBindings; + QRhiGraphicsPipelineRef mOutliningPipeline; +public: + QOutliningPassBuilder() { + mOutliningFS = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 450 + layout (location = 0) in vec2 vUV; + layout (location = 0) out vec4 outFragColor; + + layout (binding = 0) uniform sampler2D uBaseColor; + + void main() { + vec2 texOffset = 1.0 / textureSize(uBaseColor, 0); // gets size of single texel + + vec3 maxdiff = vec3(0.f); + vec3 center = texture(uBaseColor,vUV).rgb; + + maxdiff = max(maxdiff, abs(texture(uBaseColor,vUV+vec2(texOffset.x,0)).rgb - center)); + maxdiff = max(maxdiff, abs(texture(uBaseColor,vUV-vec2(texOffset.x,0)).rgb - center)); + maxdiff = max(maxdiff, abs(texture(uBaseColor,vUV+vec2(0,texOffset.y)).rgb - center)); + maxdiff = max(maxdiff, abs(texture(uBaseColor,vUV-vec2(0,texOffset.y)).rgb - center)); + + const vec4 outliningColor = vec4(1.0,0.0,0.0,1.0); + + outFragColor = length(maxdiff) > 0.1 ? outliningColor : vec4(center,1.0f); + } )"); - Q_ASSERT(fs.isValid()); - - mPipeline->setShaderStages({ - QRhiShaderStage(QRhiShaderStage::Vertex, vs), - QRhiShaderStage(QRhiShaderStage::Fragment, fs) - }); - - QRhiVertexInputLayout inputLayout; - inputLayout.setBindings({ - QRhiVertexInputBinding(2 * sizeof(float)) - }); - - inputLayout.setAttributes({ - QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0), - }); - - mPipeline->setVertexInputLayout(inputLayout); - mPipeline->setShaderResourceBindings(mShaderBindings.get()); - mPipeline->setRenderPassDescriptor(getBasePass()->getRenderPassDescriptor()); - mPipeline->create(); - } + } + void setup(QRenderGraphBuilder& builder) override { + builder.setupTexture(mColorAttachment, "Outlining", QRhiTexture::Format::RGBA32F, mInput._BaseColor->pixelSize(), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource); + builder.setupRenderTarget(mRenderTarget, "OutliningRT", QRhiTextureRenderTargetDescription(mColorAttachment.get())); + + builder.setupSampler(mSampler, "OutliningSampler", QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge); + + builder.setupShaderResourceBindings(mOutliningBindings, "OutliningBindings", { + QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage,mInput._BaseColor.get() ,mSampler.get()), + }); + QRhiGraphicsPipelineState PSO; + PSO.shaderResourceBindings = mOutliningBindings.get(); + PSO.sampleCount = mRenderTarget->sampleCount(); + PSO.renderPassDesc = mRenderTarget->renderPassDescriptor(); + QRhiGraphicsPipeline::TargetBlend targetBlends; + targetBlends.enable = true; + PSO.targetBlends = { targetBlends }; + PSO.shaderStages = { + QRhiShaderStage(QRhiShaderStage::Vertex, builder.getFullScreenVS()), + QRhiShaderStage(QRhiShaderStage::Fragment, mOutliningFS) + }; + builder.setupGraphicsPipeline(mOutliningPipeline, "OutliningPipeline", PSO); + + mOutput.OutliningResult = mColorAttachment; + } + void execute(QRhiCommandBuffer* cmdBuffer) override { + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + cmdBuffer->beginPass(mRenderTarget.get(), clearColor, dsClearValue); + cmdBuffer->setGraphicsPipeline(mOutliningPipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mRenderTarget->pixelSize().width(), mRenderTarget->pixelSize().height())); + cmdBuffer->setShaderResources(mOutliningBindings.get()); + cmdBuffer->draw(4); + cmdBuffer->endPass(); + } +}; - void onUpload(QRhiResourceUpdateBatch* batch) override { - batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); - } +class MyRenderer : public IRenderer { +private: + QTriangleRenderComponent mComp; +public: + MyRenderer() + : IRenderer({QRhi::Vulkan}) + { + addComponent(&mComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshPassOut = graphBuilder.addPassBuilder("MeshPass"); - void onRender(QRhiCommandBuffer* cmdBuffer, const QRhiViewport& viewport) override { - cmdBuffer->setGraphicsPipeline(mPipeline.get()); - cmdBuffer->setViewport(viewport); - cmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); - cmdBuffer->setVertexInput(0, 1, &vertexBindings); - cmdBuffer->draw(3); - } + QOutliningPassBuilder::Output outliningOut = graphBuilder.addPassBuilder("OutliningPass") + .setBaseColor(meshPassOut.BaseColor); + + QOutputPassBuilder::Output ret = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(outliningOut.OutliningResult); + } }; int main(int argc, char **argv){ - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QTriangleRenderComponent::Create("Triangle") - ) - ) - .end() - ); - - widget.resize({ 800,600 }); - widget.show(); + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); -} \ No newline at end of file +} + +#include "main.moc" \ No newline at end of file diff --git a/Source/2-EngineTechnology/01-Editor/Source/main.cpp b/Source/2-EngineTechnology/01-Editor/Source/main.cpp deleted file mode 100644 index 067b6539..00000000 --- a/Source/2-EngineTechnology/01-Editor/Source/main.cpp +++ /dev/null @@ -1,135 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/IRenderComponent.h" - -static float VertexData[] = { - //position(xy) - 0.0f, -0.5f, - -0.5f, 0.5f, - 0.5f, 0.5f, -}; - -class QTriangleRenderComponent : public IRenderComponent { - Q_OBJECT - Q_PROPERTY(QColor Color READ getColor WRITE setColor) - - Q_BUILDER_BEGIN(QTriangleRenderComponent) - Q_BUILDER_ATTRIBUTE(QColor,Color) - Q_BUILDER_END() -private: - QColor mColor = Qt::green; - QScopedPointer mVertexBuffer; - QScopedPointer mUniformBuffer; - QScopedPointer mShaderBindings; - QScopedPointer mPipeline; -public: - QColor getColor() const { return mColor; } - void setColor(QColor val) { mColor = val; } -protected: - void onRebuildResource() override { - mVertexBuffer.reset(mRhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(VertexData))); - mVertexBuffer->create(); - - mUniformBuffer.reset(mRhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(QVector4D))); - mUniformBuffer->create(); - } - void onRebuildPipeline() override { - mShaderBindings.reset(mRhi->newShaderResourceBindings()); - mShaderBindings->setBindings({ - QRhiShaderResourceBinding::uniformBuffer(0,QRhiShaderResourceBinding::FragmentStage,mUniformBuffer.get()) - }); - mShaderBindings->create(); - - mPipeline.reset(mRhi->newGraphicsPipeline()); - - QRhiGraphicsPipeline::TargetBlend targetBlend; - targetBlend.enable = false; - mPipeline->setTargetBlends({ QRhiGraphicsPipeline::TargetBlend() }); - - mPipeline->setSampleCount(getBasePass()->getSampleCount()); - - mPipeline->setDepthTest(false); - mPipeline->setDepthOp(QRhiGraphicsPipeline::Always); - mPipeline->setDepthWrite(false); - - QShader vs = mRhi->newShaderFromCode(QShader::VertexStage, R"(#version 440 - layout(location = 0) in vec2 position; - out gl_PerVertex { - vec4 gl_Position; - }; - void main(){ - gl_Position = vec4(position,0.0f,1.0f); - } - )"); - Q_ASSERT(vs.isValid()); - - QShader fs = mRhi->newShaderFromCode(QShader::FragmentStage, R"(#version 440 - layout(location = 0) out vec4 fragColor; - layout(binding = 0) uniform UniformBuffer{ - vec4 uColor; - }UBO; - void main(){ - fragColor = UBO.uColor; - } - )"); - Q_ASSERT(fs.isValid()); - - mPipeline->setShaderStages({ - QRhiShaderStage(QRhiShaderStage::Vertex, vs), - QRhiShaderStage(QRhiShaderStage::Fragment, fs) - }); - - QRhiVertexInputLayout inputLayout; - inputLayout.setBindings({ - QRhiVertexInputBinding(2 * sizeof(float)) - }); - - inputLayout.setAttributes({ - QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0), - }); - - mPipeline->setVertexInputLayout(inputLayout); - mPipeline->setShaderResourceBindings(mShaderBindings.get()); - mPipeline->setRenderPassDescriptor(getBasePass()->getRenderPassDescriptor()); - mPipeline->create(); - } - - void onUpload(QRhiResourceUpdateBatch* batch) override { - batch->uploadStaticBuffer(mVertexBuffer.get(), VertexData); - } - void onUpdate(QRhiResourceUpdateBatch* batch) override { - QVector4D vec4(mColor.redF(),mColor.greenF(),mColor.blueF(),mColor.alphaF()); - batch->updateDynamicBuffer(mUniformBuffer.get(), 0, sizeof(QVector4D), &vec4); - } - void onRender(QRhiCommandBuffer* cmdBuffer, const QRhiViewport& viewport) override { - cmdBuffer->setGraphicsPipeline(mPipeline.get()); - cmdBuffer->setViewport(viewport); - cmdBuffer->setShaderResources(); - const QRhiCommandBuffer::VertexInput vertexBindings(mVertexBuffer.get(), 0); - cmdBuffer->setVertexInput(0, 1, &vertexBindings); - cmdBuffer->draw(3); - } -}; - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QTriangleRenderComponent::Create("Triangle") - ) - ) - .end() - ); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} - -#include "main.moc" \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/07-PostEffect/02-MotionBlur/README.md b/Source/2-EngineTechnology/01-ImGUI/README.md similarity index 100% rename from Source/3-GraphicsTechnology/07-PostEffect/02-MotionBlur/README.md rename to Source/2-EngineTechnology/01-ImGUI/README.md diff --git a/Source/2-EngineTechnology/01-ImGUI/Source/main.cpp b/Source/2-EngineTechnology/01-ImGUI/Source/main.cpp new file mode 100644 index 00000000..4c747003 --- /dev/null +++ b/Source/2-EngineTechnology/01-ImGUI/Source/main.cpp @@ -0,0 +1,34 @@ +#include "QEngineApplication.h" +#include "QRenderWidget.h" +#include "Render/IRenderer.h" +#include "Render/RenderGraph/PassBuilder/QImGUIPassBuilder.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" + +class MyRenderer : public IRenderer { +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QImGUIPassBuilder::Output imguiOut = graphBuilder.addPassBuilder("MeshPass"). + setPaintFunctor([](ImGuiContext* Ctx) { + ImGui::SetCurrentContext(Ctx); + ImGui::ShowStyleSelector("Style"); + ImGui::ShowDemoWindow(); + }); + + QOutputPassBuilder::Output ret = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(imguiOut.ImGuiTexture); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} + diff --git a/Source/3-GraphicsTechnology/07-PostEffect/03-ChromaticAberration/README.md b/Source/2-EngineTechnology/02-DebugDraw/README.md similarity index 100% rename from Source/3-GraphicsTechnology/07-PostEffect/03-ChromaticAberration/README.md rename to Source/2-EngineTechnology/02-DebugDraw/README.md diff --git a/Source/2-EngineTechnology/02-DebugDraw/Source/main.cpp b/Source/2-EngineTechnology/02-DebugDraw/Source/main.cpp new file mode 100644 index 00000000..4c747003 --- /dev/null +++ b/Source/2-EngineTechnology/02-DebugDraw/Source/main.cpp @@ -0,0 +1,34 @@ +#include "QEngineApplication.h" +#include "QRenderWidget.h" +#include "Render/IRenderer.h" +#include "Render/RenderGraph/PassBuilder/QImGUIPassBuilder.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" + +class MyRenderer : public IRenderer { +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QImGUIPassBuilder::Output imguiOut = graphBuilder.addPassBuilder("MeshPass"). + setPaintFunctor([](ImGuiContext* Ctx) { + ImGui::SetCurrentContext(Ctx); + ImGui::ShowStyleSelector("Style"); + ImGui::ShowDemoWindow(); + }); + + QOutputPassBuilder::Output ret = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(imguiOut.ImGuiTexture); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} + diff --git a/Source/2-EngineTechnology/02-ImGUI/Source/main.cpp b/Source/2-EngineTechnology/02-ImGUI/Source/main.cpp deleted file mode 100644 index 117968de..00000000 --- a/Source/2-EngineTechnology/02-ImGUI/Source/main.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/QFrameGraph.h" -#include "Render/Pass/QImGUIRenderPass.h" - -int main(int argc, char** argv) { - qputenv("QSG_INFO", "1"); - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QImGUIRenderPass::Create("ImGui") - .setPaintFunctor([]() { - ImGui::ShowFontSelector("Font"); - ImGui::ShowStyleSelector("Style"); - ImGui::ShowDemoWindow(); - }) - ) - .end() - ); - - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} - diff --git a/Source/2-EngineTechnology/03-DebugDraw/Source/main.cpp b/Source/2-EngineTechnology/03-DebugDraw/Source/main.cpp deleted file mode 100644 index 252e79b2..00000000 --- a/Source/2-EngineTechnology/03-DebugDraw/Source/main.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/QFrameGraph.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/Component/QStaticMeshRenderComponent.h" -#include "Render/Component/QSkeletalMeshRenderComponent.h" -#include "Render/Component/QParticlesRenderComponent.h" -#include "Render/Pass/QPixelFilterRenderPass.h" -#include "Render/Pass/QBlurRenderPass.h" -#include "Render/Pass/QBloomRenderPass.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - - widget.setupCamera() - ->setPosition(QVector3D(0,100,800)); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QStaticMeshRenderComponent::Create("StaticMesh") - .setStaticMesh(QStaticMesh::CreateFromFile(RESOURCE_DIR"/Genji/Genji.FBX")) - .setTranslate(QVector3D(-200, 0, 0)) - .setScale3D(QVector3D(10, 10, 10)) - ) - .addComponent( - QSkeletalMeshRenderComponent::Create("SkeletalMesh") - .setSkeletalMesh(QSkeletalMesh::CreateFromFile(RESOURCE_DIR"/Catwalk Walk Turn 180 Tight R.fbx")) - .setTranslate(QVector3D(200, 0, 0)) - ) - .addComponent( - QParticlesRenderComponent::Create("GPU Particles") - ) - ) - .addPass( - QPixelFilterRenderPass::Create("BrightPixels") - .setTextureIn_Src("BasePass",QBasePassForward::Out::BaseColor) - .setFilterCode(R"( - const float threshold = 1.0f; - void main() { - vec4 color = texture(uTexture, vUV); - float value = max(max(color.r,color.g),color.b); - outFragColor = (1-step(value, threshold)) * color * 100; - } - )") - ) - .addPass( - QBlurRenderPass::Create("Blur") - .setBlurIterations(1) - .setTextureIn_Src("BrightPixels", QPixelFilterRenderPass::Out::Result) - ) - .addPass( - QBloomRenderPass::Create("ToneMapping") - .setTextureIn_Raw("BasePass", QBasePassForward::Out::BaseColor) - .setTextureIn_Blur("BloomBlur", QBlurRenderPass::Out::Result) - ) - .end("ToneMapping", QBloomRenderPass::Out::Result) - ); - - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} - diff --git a/Source/3-GraphicsTechnology/07-PostEffect/04-SSAO/README.md b/Source/2-EngineTechnology/03-DigitalSignalProcessing/README.md similarity index 100% rename from Source/3-GraphicsTechnology/07-PostEffect/04-SSAO/README.md rename to Source/2-EngineTechnology/03-DigitalSignalProcessing/README.md diff --git a/Source/2-EngineTechnology/03-DigitalSignalProcessing/Source/main.cpp b/Source/2-EngineTechnology/03-DigitalSignalProcessing/Source/main.cpp new file mode 100644 index 00000000..e648afb4 --- /dev/null +++ b/Source/2-EngineTechnology/03-DigitalSignalProcessing/Source/main.cpp @@ -0,0 +1,42 @@ +#include "QEngineApplication.h" +#include "QRenderWidget.h" +#include "QtConcurrent/qtconcurrentrun.h" +#include "Render/Component/Derived/QSpectrumRenderComponent.h" +#include "Render/Component/QStaticMeshRenderComponent.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" + +class MyRenderer : public IRenderer { +private: + QSpectrumRenderComponent mSpectrumComp; + QSharedPointer mMeshPass{ new QMeshPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mSpectrumComp.setAudio("Resources/Audio/MySunset.mp3"); + mSpectrumComp.setBarCount(1000); + mSpectrumComp.setTranslate(QVector3D(0, -0.5, 0)); + + addComponent(&mSpectrumComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass", mMeshPass); + + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(meshOut.BaseColor); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} diff --git a/Source/2-EngineTechnology/04-DigitalSignalProcessing/Source/main.cpp b/Source/2-EngineTechnology/04-DigitalSignalProcessing/Source/main.cpp deleted file mode 100644 index 0da21f71..00000000 --- a/Source/2-EngineTechnology/04-DigitalSignalProcessing/Source/main.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/Component/Derived/QSpectrumRenderComponent.h" -#include "Render/Pass/QPixelFilterRenderPass.h" -#include "Render/Pass/QBlurRenderPass.h" -#include "Render/Pass/QBloomRenderPass.h" -#include "Render/Pass/QToneMappingRenderPass.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - - widget.setupCamera(); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QSpectrumRenderComponent::Create("Spectrum") - .setAudio(RESOURCE_DIR"/Audio/MySunset.mp3") - .setBarCount(1000) - .setTranslate(QVector3D(0,-0.5,0)) - ) - ) - .addPass( - QPixelFilterRenderPass::Create("BrightPixels") - .setTextureIn_Src("BasePass",QBasePassForward::Out::BaseColor) - .setFilterCode(R"( - const float threshold = 1.0f; - void main() { - vec4 color = texture(uTexture, vUV); - float value = max(max(color.r,color.g),color.b); - outFragColor = (1-step(value, threshold)) * color ; - } - )") - ) - .addPass( - QBlurRenderPass::Create("Blur") - .setBlurIterations(1) - .setTextureIn_Src("BrightPixels", QPixelFilterRenderPass::Out::Result) - ) - .addPass( - QBloomRenderPass::Create("Bloom") - .setTextureIn_Raw("BasePass", QBasePassForward::Out::BaseColor) - .setTextureIn_Blur("Blur", QBlurRenderPass::Out::Result) - ) - .addPass( - QToneMappingRenderPass::Create("ToneMapping") - .setTextureIn_Src("Bloom", QBloomRenderPass::Out::Result) - ) - .end("ToneMapping", QToneMappingRenderPass::Out::Result) - ); - - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/05-ScreenSpaceReflection/README.md b/Source/2-EngineTechnology/04-VideoRendering/README.md similarity index 100% rename from Source/3-GraphicsTechnology/07-PostEffect/05-ScreenSpaceReflection/README.md rename to Source/2-EngineTechnology/04-VideoRendering/README.md diff --git a/Source/2-EngineTechnology/04-VideoRendering/Source/main.cpp b/Source/2-EngineTechnology/04-VideoRendering/Source/main.cpp new file mode 100644 index 00000000..82201528 --- /dev/null +++ b/Source/2-EngineTechnology/04-VideoRendering/Source/main.cpp @@ -0,0 +1,27 @@ +#include +#include "QRenderWidget.h" +#include "Render/Component/QStaticMeshRenderComponent.h" +#include "QtConcurrent/qtconcurrentrun.h" + +class MyRenderer : public IRenderer { +private: +public: + MyRenderer() + : IRenderer({QRhi::Vulkan}) { + } +public: +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + } +}; + +int main(int argc, char** argv) { + QApplication app(argc, argv); + QRhiHelper::InitParams initParams; + initParams.backend = QRhi::Implementation::Vulkan; + + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} + diff --git a/Source/2-EngineTechnology/05-VideoRendering/Source/main.cpp b/Source/2-EngineTechnology/05-VideoRendering/Source/main.cpp deleted file mode 100644 index 941cab8b..00000000 --- a/Source/2-EngineTechnology/05-VideoRendering/Source/main.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/Pass/QVideoRenderPass.h" -#include "Render/Pass/QPixelFilterRenderPass.h" -#include "Render/Pass/QBlurRenderPass.h" -#include "Render/Pass/QBloomRenderPass.h" -#include "Render/Pass/QToneMappingRenderPass.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QVideoRenderPass::Create("Video") - .setVideoPath(QUrl::fromLocalFile(RESOURCE_DIR"/Video/BadApple.mp4")) - ) - .addPass( - QPixelFilterRenderPass::Create("Outline") - .setTextureIn_Src("Video", QVideoRenderPass::Out::Output) - .setFilterCode(R"( - const float threshold = 0.05f; - void main() { - vec2 texOffset = 1.0 / textureSize(uTexture, 0); - int count = 0; - float center = texture(uTexture, vUV).r; - count += abs(texture(uTexture,vUV+vec2(texOffset.x,0)).r-center)>threshold ? 1 : 0; - count += abs(texture(uTexture,vUV-vec2(texOffset.x,0)).r-center)>threshold ? 1 : 0; - count += abs(texture(uTexture,vUV+vec2(0,texOffset.y)).r-center)>threshold ? 1 : 0; - count += abs(texture(uTexture,vUV-vec2(0,texOffset.y)).r-center)>threshold ? 1 : 0; - if(count>0&&count<5){ - outFragColor = vec4(vUV.x * count,vUV.y * count,0.5,1.0); - } - else{ - outFragColor = vec4(0); - } - } - )") - ) - .addPass( - QBlurRenderPass::Create("Blur") - .setBlurIterations(1) - .setTextureIn_Src("Outline", QPixelFilterRenderPass::Out::Result) - ) - .addPass( - QToneMappingRenderPass::Create("ToneMapping") - .setTextureIn_Src("Blur", QBlurRenderPass::Out::Result) - .setExposure(7) - .setGamma(0.5) - ) - .end("ToneMapping", QToneMappingRenderPass::Out::Result) - ); - - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/00-Spline/Source/main.cpp b/Source/3-GraphicsTechnology/00-Spline/Source/main.cpp index 7c9ca622..5b37077c 100644 --- a/Source/3-GraphicsTechnology/00-Spline/Source/main.cpp +++ b/Source/3-GraphicsTechnology/00-Spline/Source/main.cpp @@ -1,13 +1,49 @@ -#include +#include "QEngineApplication.h" +#include "QtConcurrent/qtconcurrentrun.h" #include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" +#include "Render/Component/QSplineRenderComponent.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" + +class MyRenderer : public IRenderer { +private: + QSplineRenderComponent mSplineComp; + QSharedPointer mMeshPass{ new QMeshPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + QList points; + + int numOfPoint = 10; + int scaleFactor = 10; + + for (int i = 0; i < numOfPoint; i++) { + QSplinePoint point; + point.mPoint.setX((i - numOfPoint / 2) * scaleFactor); + point.mPoint.setY(i % 2 ? scaleFactor : -scaleFactor); + points << point; + } + + mSplineComp.setPoints(points); + + addComponent(&mSplineComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass", mMeshPass); + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(meshOut.BaseColor); + } +}; int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); -} +} \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/01-Text/Source/main.cpp b/Source/3-GraphicsTechnology/01-Text/Source/main.cpp index 373419f2..5fcc6b71 100644 --- a/Source/3-GraphicsTechnology/01-Text/Source/main.cpp +++ b/Source/3-GraphicsTechnology/01-Text/Source/main.cpp @@ -1,36 +1,44 @@ -#include +#include "QEngineApplication.h" +#include "QtConcurrent/qtconcurrentrun.h" #include "QRenderWidget.h" -#include "Render/QFrameGraph.h" -#include "Render/Pass/QBasePassForward.h" #include "Render/Component/QStaticMeshRenderComponent.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" + +class MyRenderer : public IRenderer { +private: + QStaticMeshRenderComponent mTextTextureComp; + QStaticMeshRenderComponent mTextMeshComp; + QSharedPointer mMeshPass{ new QMeshPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + //QtConcurrent::run([this]() { + + //}); + mTextTextureComp.setStaticMesh(QStaticMesh::CreateFromText("TextTexture", QFont("微软雅黑", 64), Qt::white, Qt::Horizontal, 2, true)); + mTextMeshComp.setStaticMesh(QStaticMesh::CreateFromText("TextMesh", QFont("微软雅黑", 64), Qt::white, Qt::Horizontal, 2, false)); + addComponent(&mTextTextureComp); + addComponent(&mTextMeshComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass", mMeshPass); -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - widget.setupCamera() - ->setPosition(QVector3D(0,0, 2000)); - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QStaticMeshRenderComponent::Create("TextTexture") - .setStaticMesh(QStaticMesh::CreateFromText("TextTexture",QFont("微软雅黑",64), Qt::white, Qt::Horizontal, 2, true)) - .setTranslate(QVector3D(0, 100, 0)) - ) - .addComponent( - QStaticMeshRenderComponent::Create("TextMesh") - .setStaticMesh(QStaticMesh::CreateFromText("TextMesh", QFont("微软雅黑", 64), Qt::white, Qt::Horizontal, 2, false)) - .setTranslate(QVector3D(0,-100,0)) - ) - ) - .end("BasePass", QBasePassForward::BaseColor) - ); - widget.resize({ 800,600 }); - widget.show(); + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(meshOut.BaseColor); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); } - diff --git a/Source/3-GraphicsTechnology/02-Skybox/Source/main.cpp b/Source/3-GraphicsTechnology/02-Skybox/Source/main.cpp index e88ccf98..d36401f9 100644 --- a/Source/3-GraphicsTechnology/02-Skybox/Source/main.cpp +++ b/Source/3-GraphicsTechnology/02-Skybox/Source/main.cpp @@ -1,27 +1,33 @@ #include #include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/Pass/QBlurRenderPass.h" -#include "Render/Pass/QSkyRenderPass.h" +#include "Render/RenderGraph/PassBuilder/QSkyPassBuilder.h" +#include "QEngineApplication.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" -int main(int argc, char** argv) { - QApplication app(argc, argv); +class MyRenderer : public IRenderer { +private: + QSharedPointer mSkyPass{ new QSkyPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mSkyPass->setSkyBoxImageByPath("Resources/Image/environment.hdr"); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QSkyPassBuilder::Output skyOut + = graphBuilder.addPassBuilder("SkyPass", mSkyPass); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Vulkan; - QRenderWidget widget(initParams); + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(skyOut.SkyTexture); + } +}; - widget.setupCamera(); - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass - ( - QSkyRenderPass::Create("Sky") - .setSkyBoxImagePath(RESOURCE_DIR"/Image/environment.hdr") - ) - .end("Sky", QSkyRenderPass::Out::SkyTexture) - ); - widget.resize({ 800,600 }); - widget.show(); +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); -} +} \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/03-StaticMesh/Source/main.cpp b/Source/3-GraphicsTechnology/03-StaticMesh/Source/main.cpp index efbba29f..e7181796 100644 --- a/Source/3-GraphicsTechnology/03-StaticMesh/Source/main.cpp +++ b/Source/3-GraphicsTechnology/03-StaticMesh/Source/main.cpp @@ -1,33 +1,39 @@ -#include +#include "QEngineApplication.h" #include "QRenderWidget.h" -#include "Render/QFrameGraph.h" -#include "Render/Pass/QBasePassForward.h" +#include "QtConcurrent/qtconcurrentrun.h" #include "Render/Component/QStaticMeshRenderComponent.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" -int main(int argc, char **argv){ - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); +class MyRenderer : public IRenderer { +private: + QStaticMeshRenderComponent mStaticComp; + QSharedPointer mMeshPass{ new QMeshPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + QtConcurrent::run([this]() { + mStaticComp.setStaticMesh(QStaticMesh::CreateFromFile("Resources/Model/mandalorian_ship/scene.gltf")); + }); - auto camera = widget.setupCamera(); - camera->setPosition(QVector3D(30,25,20)); - camera->setRotation(QVector3D(-30,145,0)); + addComponent(&mStaticComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass", mMeshPass); - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QStaticMeshRenderComponent::Create("StaticMesh") - .setStaticMesh(QStaticMesh::CreateFromFile(RESOURCE_DIR"/Model/mandalorian_ship/scene.gltf")) - .setRotation(QVector3D(-90,0,0)) - ) - ) - .end("BasePass", QBasePassForward::BaseColor) - ); - widget.resize({ 800,600 }); - widget.show(); + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(meshOut.BaseColor); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); } - diff --git a/Source/3-GraphicsTechnology/04-SkeletonMesh/Source/main.cpp b/Source/3-GraphicsTechnology/04-SkeletonMesh/Source/main.cpp index 0871664f..b1113a4e 100644 --- a/Source/3-GraphicsTechnology/04-SkeletonMesh/Source/main.cpp +++ b/Source/3-GraphicsTechnology/04-SkeletonMesh/Source/main.cpp @@ -1,32 +1,39 @@ -#include +#include "QEngineApplication.h" #include "QRenderWidget.h" -#include "Render/QFrameGraph.h" -#include "Render/Pass/QBasePassForward.h" +#include "QtConcurrent/qtconcurrentrun.h" #include "Render/Component/QSkeletalMeshRenderComponent.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" -int main(int argc, char **argv){ - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); +class MyRenderer : public IRenderer { +private: + QSkeletalMeshRenderComponent mSkeletonComp; + QSharedPointer mMeshPass{ new QMeshPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mSkeletonComp.setSkeletalMesh(QSkeletalMesh::CreateFromFile("Resources/Model/Catwalk Walk Turn 180 Tight R.fbx")); + + getCamera()->setPosition(QVector3D(0, 190, -700)); + getCamera()->setRotation(QVector3D(-5, 265, 0)); + addComponent(&mSkeletonComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass", mMeshPass); - auto camera = widget.setupCamera(); - camera->setPosition(QVector3D(0, 190, -700)); - camera->setRotation(QVector3D(-5, 265, 0)); + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(meshOut.BaseColor); + } +}; - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QSkeletalMeshRenderComponent::Create("SkeletalMesh") - .setSkeletalMesh(QSkeletalMesh::CreateFromFile(RESOURCE_DIR"/Model/Catwalk Walk Turn 180 Tight R.fbx")) - ) - ) - .end("BasePass", QBasePassForward::BaseColor) - ); - - widget.resize({ 800,600 }); - widget.show(); +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); } diff --git a/Source/3-GraphicsTechnology/05-GPUParticles/Source/main.cpp b/Source/3-GraphicsTechnology/05-GPUParticles/Source/main.cpp index b49bc2b8..9f86bbb9 100644 --- a/Source/3-GraphicsTechnology/05-GPUParticles/Source/main.cpp +++ b/Source/3-GraphicsTechnology/05-GPUParticles/Source/main.cpp @@ -1,28 +1,145 @@ -#include +#include "QEngineApplication.h" #include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" +#include "QRandomGenerator" #include "Render/Component/QParticlesRenderComponent.h" +#include "QPainter" +#include "Asset/QParticleEmitter.h" + +class MyGpuParticleEmitter : public QGpuParticleEmitter { + Q_OBJECT + Q_PROPERTY(QImage PositionSampleImage READ getImage WRITE setImage) +private: + QImage mImage; + QImage getImage() const { return mImage; } + void setImage(QImage val) { mImage = val; } +public: + MyGpuParticleEmitter() { + mImage = QImage(200, 200, QImage::Format_Alpha8); + mImage.fill(Qt::transparent); + QPainter painter(&mImage); + painter.setBrush(Qt::white); + painter.setFont(QFont("", 200)); + painter.drawText(QRect(0, 0, 200, 200), Qt::AlignCenter, "GPU"); + painter.end(); + mImage.mirror(); + QGpuParticleEmitter::InitParams params; + QVector positionPool; + for (int i = 0; i < mImage.width(); i++) { + for (int j = 0; j < mImage.height(); j++) { + QColor color = mImage.pixelColor(i, j); + if (color.alpha()) { + positionPool << QVector4D(i / (float)mImage.width() - 0.5, j / (float)mImage.height() - 0.5f, 0, 1) * 3; + } + } + } + params.spawnParams->addParam("MinSize", 0.0f); + params.spawnParams->addParam("MaxSize", 0.001f); + params.spawnParams->addParam("PositionPool", positionPool, false); + + params.spawnDefine = R"( + float rand(float seed, float min, float max){ + return min + (max-min) * fract(sin(dot(vec2(gl_GlobalInvocationID.x * seed * UpdateCtx.deltaSec,UpdateCtx.timestamp) ,vec2(12.9898,78.233))) * 43758.5453); + } + )"; + + params.spawnCode = R"( + outParticle.age = 0.0f; + outParticle.lifetime = 3.0f; + outParticle.scaling = vec3(rand(0.45,Params.MinSize,Params.MaxSize)); + + float noiseStrength = 0.01; + vec3 noiseOffset = vec3(rand(0.12,-noiseStrength,noiseStrength),rand(0.11561,-noiseStrength,noiseStrength),0); + outParticle.position = Params.PositionPool[int(rand(0.234212,0,Params.PositionPool.length()))].xyz + noiseOffset; + + outParticle.velocity = vec3(0,rand(0.24324,0,0.0001),rand(23.4451,-0.00005,0.000000)); + )"; + + params.updateDefine = R"( + float rand(float seed, float min, float max){ + return min + (max-min) * fract(sin(dot(vec2(gl_GlobalInvocationID.x * seed * UpdateCtx.deltaSec,UpdateCtx.timestamp) ,vec2(12.9898,78.233))) * 43758.5453); + } + )"; + + params.updateCode = R"( + outParticle.age = inParticle.age + UpdateCtx.deltaSec; + outParticle.position = inParticle.position + inParticle.velocity; + outParticle.velocity = inParticle.velocity + 0.001*vec3(rand(42.2135,-0.0001,0.0001),0,0); + outParticle.scaling = inParticle.scaling; + outParticle.rotation = inParticle.rotation; + )"; + setupParams(params); + } +}; + +class MyCpuParticleEmitter : public QCpuParticleEmitter { + Q_OBJECT + Q_PROPERTY(float MinSize READ getMinSize WRITE setMinSize) + Q_PROPERTY(float MaxSize READ getMaxSize WRITE setMaxSize) + +public: + float getMinSize() const { return mMinSize; } + void setMinSize(float val) { mMinSize = val; } + float getMaxSize() const { return mMaxSize; } + void setMaxSize(float val) { mMaxSize = val; } + float rand(float min, float max) {return mRandom.generateDouble() * (max - min) + min;} + MyCpuParticleEmitter() { + setNumOfSpawnPerFrame(10); + } +private: + QRandomGenerator mRandom; + float mMinSize = 0.0f; + float mMaxSize = 0.001f; +protected: + void onSpawn(Particle& outParticle) override { + outParticle.age = 0.0f; + outParticle.lifetime = 2.0f; + float uniformScale = rand(mMinSize, mMaxSize); + outParticle.scaling = QVector3D(uniformScale, uniformScale, uniformScale); + outParticle.position = QVector3D(rand(-2, 2), rand(-2, 2), rand(-2,2)); + outParticle.velocity = QVector3D(0.0, 0, rand(0.0001, 0.01)); + } + void onUpdate(Particle& outParticle) override { + if (outParticle.position.z() > 1) + outParticle.age = outParticle.lifetime; + } +}; + + +class MyRenderer : public IRenderer { +private: + QParticlesRenderComponent mCpuParticlesComp; + QParticlesRenderComponent mGpuParticlesComp; + QSharedPointer mMeshPass{ new QMeshPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mCpuParticlesComp.setEmitter(new MyCpuParticleEmitter); + mCpuParticlesComp.setParticleShape(QStaticMesh::CreateFromText("CPU", QFont())); + mGpuParticlesComp.setEmitter(new MyGpuParticleEmitter); + + addComponent(&mCpuParticlesComp); + addComponent(&mGpuParticlesComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass", mMeshPass); + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(meshOut.BaseColor); + } +}; int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - - widget.setupCamera(); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QParticlesRenderComponent::Create("Particles") - ) - ) - .end("BasePass",QBasePassForward::BaseColor) - ); - - widget.resize({ 800,600 }); - widget.show(); + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); } + +#include "main.moc" \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/06-ShaderToy/Source/main.cpp b/Source/3-GraphicsTechnology/06-ShaderToy/Source/main.cpp index 96935710..83fe1299 100644 --- a/Source/3-GraphicsTechnology/06-ShaderToy/Source/main.cpp +++ b/Source/3-GraphicsTechnology/06-ShaderToy/Source/main.cpp @@ -1,130 +1,141 @@ -#include +#include "QEngineApplication.h" #include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/Pass/QGlslSandboxRenderPass.h" - - -int main(int argc, char** argv) { - QApplication app(argc, argv); - - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QGlslSandboxRenderPass::Create("GlslSandbox") - .setShaderCode(R"( -/* - * Original shader from: https://www.shadertoy.com/view/mtBSWc - */ - -#ifdef GL_ES -precision highp float; -#endif - -// glslsandbox uniforms -uniform float time; -uniform vec2 resolution; - -// shadertoy emulation -#define iTime time -#define iResolution resolution - -// --------[ Original ShaderToy begins here ]---------- // -//philip.bertani@gmail.com - -const float numOct = 5. ; //number of fbm octaves -float focus = 0.; -float focus2 = 0.; -#define pi 3.14159265 - -float random(vec2 p) { - //a random modification of the one and only random() func - return fract( sin( dot( p, vec2(12., 90.)))* 5e5 ); -} - -mat2 rot2(float an){float cc=cos(an),ss=sin(an); return mat2(cc,-ss,ss,cc);} - -//this is taken from Visions of Chaos shader "Sample Noise 2D 4.glsl" -float noise(vec3 p) { - vec2 i = floor(p.yz); - vec2 f = fract(p.yz); - float a = random(i + vec2(0.,0.)); - float b = random(i + vec2(1.,0.)); - float c = random(i + vec2(0.,1.)); - float d = random(i + vec2(1.,1.)); - vec2 u = f*f*(3.-2.*f); +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QGlslSandboxPassBuilder.h" + +class MyRenderer : public IRenderer { +private: + QSharedPointer mGlslSandboxPass{ new QGlslSandboxPassBuilder }; + QByteArray mShaderCode; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mShaderCode = R"( + /* + * Original shader from: https://www.shadertoy.com/view/mtBSWc + */ + + #ifdef GL_ES + precision highp float; + #endif + + // glslsandbox uniforms + uniform float time; + uniform vec2 resolution; + + // shadertoy emulation + #define iTime time + #define iResolution resolution + + // --------[ Original ShaderToy begins here ]---------- // + //philip.bertani@gmail.com + + const float numOct = 5. ; //number of fbm octaves + float focus = 0.; + float focus2 = 0.; + #define pi 3.14159265 + + float random(vec2 p) { + //a random modification of the one and only random() func + return fract( sin( dot( p, vec2(12., 90.)))* 5e5 ); + } + + mat2 rot2(float an){float cc=cos(an),ss=sin(an); return mat2(cc,-ss,ss,cc);} + + //this is taken from Visions of Chaos shader "Sample Noise 2D 4.glsl" + float noise(vec3 p) { + vec2 i = floor(p.yz); + vec2 f = fract(p.yz); + float a = random(i + vec2(0.,0.)); + float b = random(i + vec2(1.,0.)); + float c = random(i + vec2(0.,1.)); + float d = random(i + vec2(1.,1.)); + vec2 u = f*f*(3.-2.*f); - return mix( mix(a,b,u.x), mix(c,d,u.x), u.y); -} + return mix( mix(a,b,u.x), mix(c,d,u.x), u.y); + } -float fbm3d(vec3 p) { - float v = 0.; - float a = .5; - vec3 shift = vec3(focus - focus2); //play with this + float fbm3d(vec3 p) { + float v = 0.; + float a = .5; + vec3 shift = vec3(focus - focus2); //play with this - float angle = pi/1.3 + .03*focus; //play with this + float angle = pi/1.3 + .03*focus; //play with this - for (float i=0.; i("OutputPass") + .setInitialTexture(glslOut.GlslSandboxResult); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); -} +} \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/07-PostEffect/00-Blur/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/00-Blur/Source/main.cpp index 199c9dff..cce4c0ce 100644 --- a/Source/3-GraphicsTechnology/07-PostEffect/00-Blur/Source/main.cpp +++ b/Source/3-GraphicsTechnology/07-PostEffect/00-Blur/Source/main.cpp @@ -1,39 +1,66 @@ -#include +#include "QEngineApplication.h" #include "QRenderWidget.h" -#include "Render/QFrameGraph.h" -#include "Render/Pass/QBasePassForward.h" +#include "QtConcurrent/qtconcurrentrun.h" #include "Render/Component/QStaticMeshRenderComponent.h" -#include "Render/Pass/QBlurRenderPass.h" - -int main(int argc, char **argv){ - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - widget.setupCamera() - ->setPosition(QVector3D(0,0,25)); - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QStaticMeshRenderComponent::Create("StaticMesh") - .setStaticMesh(QStaticMesh::CreateFromFile(RESOURCE_DIR"/Model/mandalorian/scene.gltf")) - .setTranslate(QVector3D(0,-5,0)) - .setRotation(QVector3D(-90,0,0)) - ) - ) - .addPass( - QBlurRenderPass::Create("Blur") - .setBlurIterations(2) - .setBlurSize(10) - .setDownSampleCount(4) - .setTextureIn_Src( "BasePass", QBasePassForward::Out::BaseColor) - ) - .end("Blur", QBlurRenderPass::Result) - ); - widget.resize({ 800,600 }); - widget.show(); +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QBlurPassBuilder.h" + +#define Q_PROPERTY_VAR(Type,Name)\ + Q_PROPERTY(Type Name READ get_##Name WRITE set_##Name) \ + Type get_##Name(){ return Name; } \ + void set_##Name(Type var){ \ + Name = var; \ + } \ + Type Name + +class MyRenderer : public IRenderer { + Q_OBJECT + Q_PROPERTY_VAR(int, BlurIterations) = 2; + Q_PROPERTY_VAR(int, BlurSize) = 20; + Q_PROPERTY_VAR(int, DownSampleCount) = 4; + + Q_CLASSINFO("BlurIterations", "Min=1,Max=8") + Q_CLASSINFO("BlurSize", "Min=1,Max=80") + Q_CLASSINFO("DownSampleCount", "Min=1,Max=16") +private: + QStaticMeshRenderComponent mStaticComp; + QSharedPointer mMeshPass{ new QMeshPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mStaticComp.setStaticMesh(QStaticMesh::CreateFromFile("Resources/Model/mandalorian_ship/scene.gltf")); + mStaticComp.setRotation(QVector3D(-90, 0, 0)); + + addComponent(&mStaticComp); + + getCamera()->setPosition(QVector3D(20, 15, 12)); + getCamera()->setRotation(QVector3D(-30, 145, 0)); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass", mMeshPass); + + QBlurPassBuilder::Output blurOut = graphBuilder.addPassBuilder("BlurPass") + .setBaseColorTexture(meshOut.BaseColor) + .setBlurIterations(BlurIterations) + .setBlurSize(BlurSize) + .setDownSampleCount(DownSampleCount); + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(blurOut.BlurResult); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); } +#include "main.moc" \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/07-PostEffect/01-Bloom/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/01-Bloom/Source/main.cpp index 1c408fd7..97e42605 100644 --- a/Source/3-GraphicsTechnology/07-PostEffect/01-Bloom/Source/main.cpp +++ b/Source/3-GraphicsTechnology/07-PostEffect/01-Bloom/Source/main.cpp @@ -1,56 +1,94 @@ -#include +#include "QEngineApplication.h" #include "QRenderWidget.h" +#include "QtConcurrent/qtconcurrentrun.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QMeshPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QBlurPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QPixelFilterPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QBloomPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QToneMappingPassBuilder.h" #include "Render/Component/QParticlesRenderComponent.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/Pass/QPixelFilterRenderPass.h" -#include "Render/Pass/QBlurRenderPass.h" -#include "Render/Pass/QBloomRenderPass.h" -#include "Render/Pass/QToneMappingRenderPass.h" +#include "Render/Component/QStaticMeshRenderComponent.h" -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QBasePassForward::Create("BasePass") - .addComponent( - QParticlesRenderComponent::Create("GPU Particles") - .setColor(QColor4D(0.2, 1, 1.8)) - ) - ) - .addPass( - QPixelFilterRenderPass::Create("BrightPixels") - .setTextureIn_Src("BasePass",QBasePassForward::Out::BaseColor) +#define Q_PROPERTY_VAR(Type,Name)\ + Q_PROPERTY(Type Name READ get_##Name WRITE set_##Name) \ + Type get_##Name(){ return Name; } \ + void set_##Name(Type var){ \ + Name = var; \ + } \ + Type Name + +class MyRenderer : public IRenderer { + Q_OBJECT + Q_PROPERTY_VAR(int, BlurIterations) = 2; + Q_PROPERTY_VAR(int, BlurSize) = 20; + Q_PROPERTY_VAR(int, DownSampleCount) = 4; + + Q_PROPERTY_VAR(float, Gamma) = 2.2f; + Q_PROPERTY_VAR(float, Exposure) = 1.f; + Q_PROPERTY_VAR(float, PureWhite) = 1.f; + + Q_CLASSINFO("BlurIterations", "Min=1,Max=8") + Q_CLASSINFO("BlurSize", "Min=1,Max=80") + Q_CLASSINFO("DownSampleCount", "Min=1,Max=16") +private: + QStaticMeshRenderComponent mStaticComp; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mStaticComp.setStaticMesh(QStaticMesh::CreateFromFile("Resources/Model/mandalorian_ship/scene.gltf")); + mStaticComp.setRotation(QVector3D(-90, 0, 0)); + + addComponent(&mStaticComp); + + getCamera()->setPosition(QVector3D(20, 15, 12)); + getCamera()->setRotation(QVector3D(-30, 145, 0)); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass"); + + QPixelFilterPassBuilder::Output filterOut = graphBuilder.addPassBuilder("FilterPass") + .setBaseColorTexture(meshOut.BaseColor) .setFilterCode(R"( - const float threshold = 1.0f; + const float threshold = 0.5f; void main() { vec4 color = texture(uTexture, vUV); float value = max(max(color.r,color.g),color.b); - outFragColor = (1-step(value, threshold)) * color * 100; + outFragColor = (1-step(value, threshold)) * color; } - )") - ) - .addPass( - QBlurRenderPass::Create("Blur") - .setBlurIterations(1) - .setTextureIn_Src("BrightPixels", QPixelFilterRenderPass::Out::Result) - ) - .addPass( - QBloomRenderPass::Create("Bloom") - .setTextureIn_Raw("BasePass", QBasePassForward::Out::BaseColor) - .setTextureIn_Blur("Blur", QBlurRenderPass::Out::Result) - ) - .addPass( - QToneMappingRenderPass::Create("ToneMapping") - .setTextureIn_Src("Bloom", QBloomRenderPass::Out::Result) - ) - .end("ToneMapping", QToneMappingRenderPass::Out::Result) - ); - widget.resize({ 800,600 }); - widget.show(); + )"); + + QBlurPassBuilder::Output blurOut = graphBuilder.addPassBuilder("BlurPass") + .setBaseColorTexture(filterOut.FilterResult) + .setBlurIterations(BlurIterations) + .setBlurSize(BlurSize) + .setDownSampleCount(DownSampleCount); + + QBloomPassBuilder::Output bloomOut = graphBuilder.addPassBuilder("BloomPass") + .setBaseColorTexture(meshOut.BaseColor) + .setBlurTexture(blurOut.BlurResult); + + QToneMappingPassBuilder::Output tonemappingOut = graphBuilder.addPassBuilder("ToneMappingPass") + .setBaseColorTexture(bloomOut.BloomResult) + .setExposure(Exposure) + .setGamma(Gamma) + .setPureWhite(PureWhite); + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(tonemappingOut.ToneMappingReslut); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); } + +#include "main.moc" \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/07-PostEffect/02-MotionBlur/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/02-MotionBlur/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/02-MotionBlur/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/06-ScreenSpaceRefraction/README.md b/Source/3-GraphicsTechnology/07-PostEffect/02-Outlining/README.md similarity index 100% rename from Source/3-GraphicsTechnology/07-PostEffect/06-ScreenSpaceRefraction/README.md rename to Source/3-GraphicsTechnology/07-PostEffect/02-Outlining/README.md diff --git a/Source/3-GraphicsTechnology/07-PostEffect/02-Outlining/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/02-Outlining/Source/main.cpp new file mode 100644 index 00000000..0e74f8b4 --- /dev/null +++ b/Source/3-GraphicsTechnology/07-PostEffect/02-Outlining/Source/main.cpp @@ -0,0 +1,69 @@ +#include "QEngineApplication.h" +#include "QRenderWidget.h" +#include "QtConcurrent/qtconcurrentrun.h" +#include "Render/Component/QStaticMeshRenderComponent.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/PBR/QPbrMeshPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QOutliningPassBuilder.h" + +#define Q_PROPERTY_VAR(Type,Name)\ + Q_PROPERTY(Type Name READ get_##Name WRITE set_##Name) \ + Type get_##Name(){ return Name; } \ + void set_##Name(Type var){ \ + Name = var; \ + } \ + Type Name + +class MyRenderer : public IRenderer { + Q_OBJECT + Q_PROPERTY_VAR(int, Radius) = 2; + Q_PROPERTY_VAR(QColor4D, ColorModifier) = QColor4D(0.324f, 0.063f, 0.099f, 1.0f); + Q_PROPERTY_VAR(float, MinSeparation) = 1.0f; + Q_PROPERTY_VAR(float, MaxSeparation) = 3.0f; + Q_PROPERTY_VAR(float, MinDistance) = 0.5f; + Q_PROPERTY_VAR(float, MaxDistance) = 2.0f; +private: + QStaticMeshRenderComponent mStaticComp; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mStaticComp.setStaticMesh(QStaticMesh::CreateFromFile("Resources/Model/mandalorian_ship/scene.gltf")); + mStaticComp.setRotation(QVector3D(-90, 0, 0)); + + addComponent(&mStaticComp); + + getCamera()->setPosition(QVector3D(20, 15, 12)); + getCamera()->setRotation(QVector3D(-30, 145, 0)); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QPbrMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass"); + + QOutliningPassBuilder::Output outliningOut = graphBuilder.addPassBuilder("OutliningPass") + .setBaseColorTexture(meshOut.BaseColor) + .setPositionTexture(meshOut.Position) + .setRadius(Radius) + .setColorModifier(ColorModifier) + .setMinDistance(MinDistance) + .setMaxDistance(MaxDistance) + .setMinSeparation(MinSeparation) + .setMaxSeparation(MaxSeparation) + ; + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(outliningOut.OutliningReslut); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} + +#include "main.moc" \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/07-PostEffect/03-ChromaticAberration/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/03-ChromaticAberration/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/03-ChromaticAberration/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/07-FlowMapping/README.md b/Source/3-GraphicsTechnology/07-PostEffect/03-SSAO/README.md similarity index 100% rename from Source/3-GraphicsTechnology/07-PostEffect/07-FlowMapping/README.md rename to Source/3-GraphicsTechnology/07-PostEffect/03-SSAO/README.md diff --git a/Source/3-GraphicsTechnology/07-PostEffect/03-SSAO/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/03-SSAO/Source/main.cpp new file mode 100644 index 00000000..53c9b5aa --- /dev/null +++ b/Source/3-GraphicsTechnology/07-PostEffect/03-SSAO/Source/main.cpp @@ -0,0 +1,152 @@ +#include "QEngineApplication.h" +#include "QRenderWidget.h" +#include "QtConcurrent/qtconcurrentrun.h" +#include "Render/Component/QStaticMeshRenderComponent.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QSsaoPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QBlurPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/PBR/QPbrMeshPassBuilder.h" + +class QSsaoMergePassBuilder : public IRenderPassBuilder { + QRP_INPUT_BEGIN(QSsaoMergePassBuilder) + QRP_INPUT_ATTR(QRhiTextureRef, BaseColor); + QRP_INPUT_ATTR(QRhiTextureRef, SsaoTexture); + QRP_INPUT_END() + + QRP_OUTPUT_BEGIN(QSsaoMergePassBuilder) + QRP_OUTPUT_ATTR(QRhiTextureRef, SsaoMergeResult) + QRP_OUTPUT_END() +private: + QRhiTextureRef mColorAttachment; + QRhiTextureRenderTargetRef mRenderTarget; + QShader mMergeFS; + QRhiSamplerRef mSampler; + QRhiShaderResourceBindingsRef mMergeBindings; + QRhiGraphicsPipelineRef mMergePipeline; +public: + QSsaoMergePassBuilder() { + mMergeFS = QRhiHelper::newShaderFromCode(QShader::FragmentStage, R"(#version 450 + layout (binding = 0) uniform sampler2D uSrcTexture; + layout (binding = 1) uniform sampler2D uSsaoTexture; + layout (location = 0) in vec2 vUV; + layout (location = 0) out vec4 outFragColor; + void main() { + vec4 srcColor = texture(uSrcTexture, vUV); + vec4 ssaoColor = texture(uSsaoTexture, vUV); + outFragColor = vec4(srcColor.rgb * ssaoColor.r,srcColor.a); + } + )"); + } + void setup(QRenderGraphBuilder& builder) override { + builder.setupTexture(mColorAttachment, "SsaoMerge", QRhiTexture::Format::RGBA32F, mInput._BaseColor->pixelSize(), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource); + builder.setupRenderTarget(mRenderTarget, "SsaoMergeRT", QRhiTextureRenderTargetDescription(mColorAttachment.get())); + + builder.setupSampler(mSampler, "SsaoMergeSampler", QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge); + + builder.setupShaderResourceBindings(mMergeBindings, "SsaoMergeBindings", { + QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage,mInput._BaseColor.get() ,mSampler.get()), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage,mInput._SsaoTexture.get() ,mSampler.get()), + }); + QRhiGraphicsPipelineState PSO; + PSO.shaderResourceBindings = mMergeBindings.get(); + PSO.sampleCount = mRenderTarget->sampleCount(); + PSO.renderPassDesc = mRenderTarget->renderPassDescriptor(); + QRhiGraphicsPipeline::TargetBlend targetBlends; + targetBlends.enable = true; + PSO.targetBlends = { targetBlends }; + PSO.shaderStages = { + QRhiShaderStage(QRhiShaderStage::Vertex, builder.getFullScreenVS()), + QRhiShaderStage(QRhiShaderStage::Fragment, mMergeFS) + }; + builder.setupGraphicsPipeline(mMergePipeline, "SsaoMergePipeline", PSO); + + mOutput.SsaoMergeResult = mColorAttachment; + } + void execute(QRhiCommandBuffer* cmdBuffer) override { + const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 0.0f, 1.0f); + const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 }; + cmdBuffer->beginPass(mRenderTarget.get(), clearColor, dsClearValue); + cmdBuffer->setGraphicsPipeline(mMergePipeline.get()); + cmdBuffer->setViewport(QRhiViewport(0, 0, mRenderTarget->pixelSize().width(), mRenderTarget->pixelSize().height())); + cmdBuffer->setShaderResources(mMergeBindings.get()); + cmdBuffer->draw(4); + cmdBuffer->endPass(); + } +}; + +#define Q_PROPERTY_VAR(Type, Name)\ + Q_PROPERTY(Type Name READ get_##Name WRITE set_##Name) \ + Type get_##Name(){ return Name; } \ + void set_##Name(Type var){ \ + Name = var; \ + } \ + Type Name + + +class MyRenderer : public IRenderer { + Q_OBJECT + + Q_PROPERTY_VAR(float, Bias) = 0.1f; + Q_PROPERTY_VAR(float, Radius) = 2.0f; + Q_PROPERTY_VAR(int, SampleSize) = 64; + + Q_PROPERTY_VAR(int, BlurIterations) = 1; + Q_PROPERTY_VAR(int, BlurSize) = 4; + Q_PROPERTY_VAR(int, DownSampleCount) = 2; + + Q_CLASSINFO("SampleSize", "Min=1,Max=128") + Q_CLASSINFO("BlurIterations", "Min=1,Max=8") + Q_CLASSINFO("BlurSize", "Min=1,Max=80") + Q_CLASSINFO("DownSampleCount", "Min=1,Max=16") +private: + QStaticMeshRenderComponent mStaticComp; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mStaticComp.setStaticMesh(QStaticMesh::CreateFromFile("Resources/Model/mandalorian_ship/scene.gltf")); + mStaticComp.setRotation(QVector3D(-90, 0, 0)); + + addComponent(&mStaticComp); + + getCamera()->setPosition(QVector3D(20, 15, 12)); + getCamera()->setRotation(QVector3D(-30, 145, 0)); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QPbrMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass"); + + QSsaoPassBuilder::Output ssaoOut = graphBuilder.addPassBuilder("SsaoPass") + .setNormalTexture(meshOut.Normal) + .setPositionTexture(meshOut.Position) + .setBias(Bias) + .setRadius(Radius) + .setSampleSize(SampleSize); + + QBlurPassBuilder::Output blurOut = graphBuilder.addPassBuilder("BlurPass") + .setBaseColorTexture(ssaoOut.SsaoResult) + .setBlurIterations(BlurIterations) + .setBlurSize(BlurSize) + .setDownSampleCount(DownSampleCount); + + QSsaoMergePassBuilder::Output merge = graphBuilder.addPassBuilder("SsaoMergePass") + .setBaseColor(meshOut.BaseColor) + .setSsaoTexture(blurOut.BlurResult); + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(merge.SsaoMergeResult); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} + + +#include "main.moc" \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/07-PostEffect/08-Outlining/README.md b/Source/3-GraphicsTechnology/07-PostEffect/04-DepthOfField/README.md similarity index 100% rename from Source/3-GraphicsTechnology/07-PostEffect/08-Outlining/README.md rename to Source/3-GraphicsTechnology/07-PostEffect/04-DepthOfField/README.md diff --git a/Source/3-GraphicsTechnology/07-PostEffect/04-DepthOfField/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/04-DepthOfField/Source/main.cpp new file mode 100644 index 00000000..5f63875e --- /dev/null +++ b/Source/3-GraphicsTechnology/07-PostEffect/04-DepthOfField/Source/main.cpp @@ -0,0 +1,139 @@ +#include "QEngineApplication.h" +#include "QRenderWidget.h" +#include +#include +#include "QtConcurrent/qtconcurrentrun.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" +#include "Render/Component/QParticlesRenderComponent.h" +#include "Render/RenderGraph/PassBuilder/PBR/QPbrMeshPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QDepthOfFieldPassBuilder.h" + +#define Q_PROPERTY_VAR(Type,Name)\ + Q_PROPERTY(Type Name READ get_##Name WRITE set_##Name) \ + Type get_##Name(){ return Name; } \ + void set_##Name(Type var){ \ + Name = var; \ + } \ + Type Name + +class MyGpuParticleEmitter : public QGpuParticleEmitter { +public: + MyGpuParticleEmitter() { + QGpuParticleEmitter::InitParams params; + + params.spawnParams->addParam("MinSize", 0.0f); + params.spawnParams->addParam("MaxSize", 0.01f); + + params.spawnDefine = R"( + float rand(float seed, float min, float max){ + return min + (max-min) * fract(sin(dot(vec2(gl_GlobalInvocationID.x * seed * UpdateCtx.deltaSec,UpdateCtx.timestamp) ,vec2(12.9898,78.233))) * 43758.5453); + } + )"; + + params.spawnCode = R"( + outParticle.age = 0.0f; + outParticle.lifetime = 3.0f; + outParticle.scaling = vec3(rand(0.45,Params.MinSize,Params.MaxSize)); + + const float noiseStrength = 10; + vec3 noiseOffset = vec3(rand(0.12,-noiseStrength,noiseStrength),0,rand(0.11561,-noiseStrength,noiseStrength)); + outParticle.position = noiseOffset; + outParticle.velocity = vec3(0,rand(0.124,0,0.05),rand(0.4451,-0.0005,0.0005)); + )"; + + params.updateDefine = R"( + float rand(float seed, float min, float max){ + return min + (max-min) * fract(sin(dot(vec2(gl_GlobalInvocationID.x * seed * UpdateCtx.deltaSec,UpdateCtx.timestamp) ,vec2(12.9898,78.233))) * 43758.5453); + } + )"; + + params.updateCode = R"( + outParticle.age = inParticle.age + UpdateCtx.deltaSec; + outParticle.position = inParticle.position + inParticle.velocity * UpdateCtx.deltaSec; + outParticle.velocity = inParticle.velocity + vec3(rand(0.41,-0.1,0.1),0,0)* UpdateCtx.deltaSec; + outParticle.scaling = inParticle.scaling; + outParticle.rotation = inParticle.rotation; + )"; + setupParams(params); + mNumOfSpawnPerFrame = 1000; + } + void onUpdateAndRecyle(QRhiCommandBuffer* inCmdBuffer) override { + QGpuParticleEmitter::onUpdateAndRecyle(inCmdBuffer); + } +}; + +class MyRenderer : public IRenderer { + Q_OBJECT + Q_PROPERTY_VAR(float, Focus) = 0.05f; + Q_PROPERTY_VAR(float, FocalLength) = 20.0f; + Q_PROPERTY_VAR(float, Aperture) = 10.5f; + Q_PROPERTY_VAR(int, ApertureBlades) = 5; + Q_PROPERTY_VAR(float, BokehSqueeze) = 0.0f; + Q_PROPERTY_VAR(float, BokehSqueezeFalloff) = 1.0f; + Q_PROPERTY_VAR(int, Iterations) = 64; +private: + QParticlesRenderComponent mParticlesComp; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + QImage image(100, 100, QImage::Format_RGBA8888); + image.fill(Qt::transparent); + QPainter painter(&image); + QPoint center(50, 50); + QPainterPath path; + for (int i = 0; i < 5; i++) { + float x1 = qCos((18 + 72 * i) * M_PI / 180) * 50, + y1 = qSin((18 + 72 * i) * M_PI / 180) * 50, + x2 = qCos((54 + 72 * i) * M_PI / 180) * 20, + y2 = qSin((54 + 72 * i) * M_PI / 180) * 20; + + if (i == 0) { + path.moveTo(x1 + center.x(), y1 + center.y()); + path.lineTo(x2 + center.x(), y2 + center.y()); + } + else { + path.lineTo(x1 + center.x(), y1 + center.y()); + path.lineTo(x2 + center.x(), y2 + center.y()); + } + } + path.closeSubpath(); + painter.fillPath(path, Qt::white); + mParticlesComp.setParticleShape(QStaticMesh::CreateFromImage(image)); + mParticlesComp.setEmitter(new MyGpuParticleEmitter); + + addComponent(&mParticlesComp); + + getCamera()->setPosition(QVector3D(0.0f, 0.1f, 10.0f)); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QPbrMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("MeshPass"); + + QDepthOfFieldPassBuilder::Output dofOut = graphBuilder.addPassBuilder("DepthOfFieldPass") + .setBaseColorTexture(meshOut.BaseColor) + .setPositionTexture(meshOut.Position) + .setFocus(Focus) + .setFocalLength(FocalLength) + .setAperture(Aperture) + .setApertureBlades(ApertureBlades) + .setBokehSqueeze(BokehSqueeze) + .setBokehSqueezeFalloff(BokehSqueezeFalloff) + .setIterations(Iterations); + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(dofOut.DepthOfFieldResult); + } +}; + +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); + return app.exec(); +} + +#include "main.moc" \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/07-PostEffect/04-SSAO/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/04-SSAO/Source/main.cpp deleted file mode 100644 index cf5f5d56..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/04-SSAO/Source/main.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/QFrameGraph.h" -#include "Render/Component/QStaticMeshRenderComponent.h" -#include "Render/Pass/PBR/QPbrBasePassDeferred.h" -#include "Render/Pass/QSsaoRenderPass.h" -#include "Render/Pass/QBlurRenderPass.h" - -int main(int argc, char **argv){ - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - - auto camera = widget.setupCamera(); - camera->setPosition(QVector3D(20, 15, 12)); - camera->setRotation(QVector3D(-30, 145, 0)); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QPbrBasePassDeferred::Create("BasePass") - .addComponent( - QStaticMeshRenderComponent::Create("StaticMesh") - .setStaticMesh(QStaticMesh::CreateFromFile(RESOURCE_DIR"/Model/mandalorian_ship/scene.gltf")) - .setRotation(QVector3D(-90, 0, 0)) - ) - ) - .addPass( - QSsaoRenderPass::Create("SSAO") - .setTextureIn_Position("BasePass", QPbrBasePassDeferred::Out::Position) - .setTextureIn_Normal("BasePass", QPbrBasePassDeferred::Out::Normal) - ) - .addPass( - QBlurRenderPass::Create("Blur") - .setBlurIterations(1) - .setBlurSize(4) - .setDownSampleCount(2) - .setTextureIn_Src( "SSAO", QSsaoRenderPass::Out::Result) - ) - .end("Blur", QBlurRenderPass::Result) - ); - - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} - diff --git a/Source/3-GraphicsTechnology/07-PostEffect/05-ScreenSpaceReflection/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/05-ScreenSpaceReflection/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/05-ScreenSpaceReflection/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/06-ScreenSpaceRefraction/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/06-ScreenSpaceRefraction/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/06-ScreenSpaceRefraction/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/07-FlowMapping/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/07-FlowMapping/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/07-FlowMapping/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/08-Outlining/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/08-Outlining/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/08-Outlining/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/09-OutliningByBlur/README.md b/Source/3-GraphicsTechnology/07-PostEffect/09-OutliningByBlur/README.md deleted file mode 100644 index 8b137891..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/09-OutliningByBlur/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Source/3-GraphicsTechnology/07-PostEffect/09-OutliningByBlur/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/09-OutliningByBlur/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/09-OutliningByBlur/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/10-DepthOfField/README.md b/Source/3-GraphicsTechnology/07-PostEffect/10-DepthOfField/README.md deleted file mode 100644 index 8b137891..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/10-DepthOfField/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Source/3-GraphicsTechnology/07-PostEffect/10-DepthOfField/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/10-DepthOfField/Source/main.cpp deleted file mode 100644 index 06b299bb..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/10-DepthOfField/Source/main.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/QFrameGraph.h" -#include "Render/Component/QStaticMeshRenderComponent.h" -#include "Render/Pass/QDilationRenderPass.h" -#include "Render/Pass/QDepthOfFieldRenderPass.h" -#include "Render/Pass/PBR/QPbrBasePassDeferred.h" - -int main(int argc, char **argv){ - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Implementation::Vulkan; - QRenderWidget widget(initParams); - widget.setupCamera(); - - auto StaticMesh = QStaticMesh::CreateFromFile(RESOURCE_DIR"/cerberus.fbx"); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QPbrBasePassDeferred::Create("BasePass") - .addComponent( - QStaticMeshRenderComponent::Create("StaticMesh") - .setStaticMesh(StaticMesh) - ) - ) - .addPass( - QDilationRenderPass::Create("Dilation") - .setTextureIn_Src("BasePass", QPbrBasePassDeferred::Out::BaseColor) - ) - .addPass( - QDepthOfFieldRenderPass::Create("DepthOfField") - .setTextureIn_Focus("BasePass", QPbrBasePassDeferred::Out::BaseColor) - .setTextureIn_LoseFocus("Dilation", QDilationRenderPass::Out::Result) - .setTextureIn_Position("BasePass", QPbrBasePassDeferred::Out::Position) - ) - .end("DepthOfField", QDepthOfFieldRenderPass::Result) - ); - - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} - diff --git a/Source/3-GraphicsTechnology/07-PostEffect/11-Posterization/README.md b/Source/3-GraphicsTechnology/07-PostEffect/11-Posterization/README.md deleted file mode 100644 index 8b137891..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/11-Posterization/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Source/3-GraphicsTechnology/07-PostEffect/11-Posterization/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/11-Posterization/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/11-Posterization/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/12-Pixelization/README.md b/Source/3-GraphicsTechnology/07-PostEffect/12-Pixelization/README.md deleted file mode 100644 index 8b137891..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/12-Pixelization/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Source/3-GraphicsTechnology/07-PostEffect/12-Pixelization/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/12-Pixelization/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/12-Pixelization/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/07-PostEffect/13-Sharpen/README.md b/Source/3-GraphicsTechnology/07-PostEffect/13-Sharpen/README.md deleted file mode 100644 index 8b137891..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/13-Sharpen/README.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Source/3-GraphicsTechnology/07-PostEffect/13-Sharpen/Source/main.cpp b/Source/3-GraphicsTechnology/07-PostEffect/13-Sharpen/Source/main.cpp deleted file mode 100644 index 7c9ca622..00000000 --- a/Source/3-GraphicsTechnology/07-PostEffect/13-Sharpen/Source/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "QRenderWidget.h" -#include "Render/Pass/QBasePassForward.h" - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QRhiWindow::InitParams initParams; - QRenderWidget widget(initParams); - widget.setupCamera(); - widget.resize({ 800,600 }); - widget.show(); - return app.exec(); -} diff --git a/Source/3-GraphicsTechnology/08-BlinnPhong/Source/main.cpp b/Source/3-GraphicsTechnology/08-BlinnPhong/Source/main.cpp index 54e70b51..4d7b2272 100644 --- a/Source/3-GraphicsTechnology/08-BlinnPhong/Source/main.cpp +++ b/Source/3-GraphicsTechnology/08-BlinnPhong/Source/main.cpp @@ -1,55 +1,59 @@ #include +#include "QtConcurrent/qtconcurrentrun.h" #include "QRenderWidget.h" -#include "Asset/QStaticMesh.h" -#include "Render/Component/Light/QPointLightComponent.h" #include "Render/Component/QStaticMeshRenderComponent.h" -#include "Render/Pass/BlinnPhong/QBlinnPhongBasePassDeferred.h" -#include "Render/Pass/BlinnPhong/QBlinnPhongLightingPass.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/Pass/QSkyRenderPass.h" +#include "Render/RenderGraph/PassBuilder/PBR/QPbrMeshPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/PBR/QPbrLightingPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QSkyPassBuilder.h" +#include "QEngineApplication.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" -int main(int argc, char** argv) { - QApplication app(argc, argv); +class MyRenderer : public IRenderer { +private: + QStaticMeshRenderComponent mStaticComp; + QSharedPointer mMeshPass{ new QPbrMeshPassBuilder }; + QSharedPointer mLightingPass{ new QPbrLightingPassBuilder }; + QSharedPointer mSkyPass{ new QSkyPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + QtConcurrent::run([this]() { + mStaticComp.setStaticMesh(QStaticMesh::CreateFromFile("Resources/Model/mandalorian_ship/scene.gltf")); + }); - QRhiWindow::InitParams initParams; + mSkyPass->setSkyBoxImageByPath("Resources/Image/environment.hdr"); - QRenderWidget widget(initParams); - auto camera = widget.setupCamera(); - camera->setPosition(QVector3D(20, 15, 12)); - camera->setRotation(QVector3D(-30, 145, 0)); + addComponent(&mStaticComp); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QSkyPassBuilder::Output skyOut + = graphBuilder.addPassBuilder("SkyPass", mSkyPass); - auto staticMesh = QStaticMesh::CreateFromFile(RESOURCE_DIR"/Model/mandalorian_ship/scene.gltf"); + QPbrMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("PbrMeshPass", mMeshPass); - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QSkyRenderPass::Create("Sky") - .setSkyBoxImagePath(RESOURCE_DIR"/Image/environment.hdr") - ) - .addPass( - QBlinnPhongBasePassDeferred::Create("BasePass") - .addComponent( - QStaticMeshRenderComponent::Create("StaticMesh") - .setStaticMesh(staticMesh) - .setRotation(QVector3D(-90,0,0)) - ) - .addComponent( - QPointLightComponent::Create("PointLight") - ) - ) - .addPass( - QBlinnPhongLightingPass::Create("Lighting") - .setTextureIn_Albedo("BasePass", QBlinnPhongBasePassDeferred::Out::BaseColor) - .setTextureIn_Position("BasePass", QBlinnPhongBasePassDeferred::Out::Position) - .setTextureIn_Normal("BasePass", QBlinnPhongBasePassDeferred::Out::Normal) - .setTextureIn_Specular("BasePass", QBlinnPhongBasePassDeferred::Out::Specular) - .setTextureIn_SkyTexture("Sky", QSkyRenderPass::Out::SkyTexture) - ) - .end("Lighting", QBlinnPhongLightingPass::Out::FragColor) - ); + QPbrLightingPassBuilder::Output lightingOut + = graphBuilder.addPassBuilder("LightingPass", mLightingPass) + .setBaseColor(meshOut.BaseColor) + .setMetallic(meshOut.Metallic) + .setNormal(meshOut.Normal) + .setPosition(meshOut.Position) + .setRoughness(meshOut.Roughness) + .setSkyCube(skyOut.SkyCube) + .setSkyTexture(skyOut.SkyTexture); - widget.resize({ 800,600 }); - widget.show(); + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(lightingOut.LightingResult); + } +}; +int main(int argc, char** argv) { + qputenv("QSG_INFO", "1"); + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); } \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/09-PBR/Source/main.cpp b/Source/3-GraphicsTechnology/09-PBR/Source/main.cpp index 29831957..4adc266e 100644 --- a/Source/3-GraphicsTechnology/09-PBR/Source/main.cpp +++ b/Source/3-GraphicsTechnology/09-PBR/Source/main.cpp @@ -1,58 +1,61 @@ #include -#include "Asset/QStaticMesh.h" +#include "QtConcurrent/qtconcurrentrun.h" #include "QRenderWidget.h" #include "Render/Component/QStaticMeshRenderComponent.h" -#include "Render/Pass/PBR/QPbrBasePassDeferred.h" -#include "Render/Pass/QBasePassForward.h" -#include "Render/Pass/QBlurRenderPass.h" -#include "Render/Pass/PBR/QPbrLightingPass.h" -#include "Render/Pass/QSkyRenderPass.h" -#include "Render/Component/Derived/QSpectrumRenderComponent.h" +#include "Render/RenderGraph/PassBuilder/PBR/QPbrMeshPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/PBR/QPbrLightingPassBuilder.h" +#include "Render/RenderGraph/PassBuilder/QSkyPassBuilder.h" +#include "QEngineApplication.h" +#include "Render/PassBuilder/QOutputPassBuilder.h" + +class MyRenderer : public IRenderer { +private: + QStaticMeshRenderComponent mStaticComp; + QSharedPointer mMeshPass{ new QPbrMeshPassBuilder }; + QSharedPointer mLightingPass{ new QPbrLightingPassBuilder }; + QSharedPointer mSkyPass{ new QSkyPassBuilder }; +public: + MyRenderer() + : IRenderer({ QRhi::Vulkan }) + { + mStaticComp.setStaticMesh(QStaticMesh::CreateFromFile("Resources/Model/mandalorian_ship/scene.gltf")); + mStaticComp.setRotation(QVector3D(-90, 0, 0)); + + mSkyPass->setSkyBoxImageByPath("Resources/Image/environment.hdr"); + + addComponent(&mStaticComp); + + getCamera()->setPosition(QVector3D(20, 15, 12)); + getCamera()->setRotation(QVector3D(-30, 145, 0)); + } +protected: + void setupGraph(QRenderGraphBuilder& graphBuilder) override { + QSkyPassBuilder::Output skyOut + = graphBuilder.addPassBuilder("SkyPass", mSkyPass); + + QPbrMeshPassBuilder::Output meshOut + = graphBuilder.addPassBuilder("PbrMeshPass",mMeshPass); + + QPbrLightingPassBuilder::Output lightingOut + = graphBuilder.addPassBuilder("LightingPass", mLightingPass) + .setBaseColor(meshOut.BaseColor) + .setMetallic(meshOut.Metallic) + .setNormal(meshOut.Normal) + .setPosition(meshOut.Position) + .setRoughness(meshOut.Roughness) + .setSkyCube(skyOut.SkyCube) + .setSkyTexture(skyOut.SkyTexture); + + QOutputPassBuilder::Output cout + = graphBuilder.addPassBuilder("OutputPass") + .setInitialTexture(lightingOut.LightingResult); + } +}; int main(int argc, char** argv) { qputenv("QSG_INFO", "1"); - - QApplication app(argc, argv); - - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::Vulkan; - QRenderWidget widget(initParams); - - auto camera = widget.setupCamera(); - camera->setPosition(QVector3D(20, 15, 12)); - camera->setRotation(QVector3D(-30, 145, 0)); - - auto staticMesh = QStaticMesh::CreateFromFile(RESOURCE_DIR"/Model/mandalorian_ship/scene.gltf"); - - widget.setFrameGraph( - QFrameGraph::Begin() - .addPass( - QSkyRenderPass::Create("Sky") - .setSkyBoxImagePath(RESOURCE_DIR"/Image/environment.hdr") - ) - .addPass( - QPbrBasePassDeferred::Create("BasePass") - .addComponent( - QStaticMeshRenderComponent::Create("StaticMesh") - .setStaticMesh(staticMesh) - .setRotation(QVector3D(-90,0,0)) - ) - ) - .addPass( - QPbrLightingPass::Create("Lighting") - .setTextureIn_Albedo("BasePass", QPbrBasePassDeferred::Out::BaseColor) - .setTextureIn_Position("BasePass", QPbrBasePassDeferred::Out::Position) - .setTextureIn_Normal("BasePass", QPbrBasePassDeferred::Out::Normal) - .setTextureIn_Metallic("BasePass", QPbrBasePassDeferred::Out::Metallic) - .setTextureIn_Roughness("BasePass", QPbrBasePassDeferred::Out::Roughness) - .setTextureIn_SkyTexture("Sky", QSkyRenderPass::Out::SkyTexture) - .setTextureIn_SkyCube("Sky", QSkyRenderPass::Out::SkyCube) - ) - .end("Lighting", QPbrLightingPass::Out::FragColor) - ); - - widget.resize({ 800,600 }); - widget.show(); - + QEngineApplication app(argc, argv); + QRenderWidget widget(new MyRenderer()); + widget.showMaximized(); return app.exec(); } \ No newline at end of file diff --git a/Source/3-GraphicsTechnology/10-RayTracing/Source/main.cpp b/Source/3-GraphicsTechnology/10-RayTracing/Source/main.cpp index 557f9a83..c25cb7fa 100644 --- a/Source/3-GraphicsTechnology/10-RayTracing/Source/main.cpp +++ b/Source/3-GraphicsTechnology/10-RayTracing/Source/main.cpp @@ -1,23 +1,10 @@ -#include -#include "Asset/QStaticMesh.h" -#include "QRenderWidget.h" -#include "Render/Component/QStaticMeshRenderComponent.h" +#include +#include int main(int argc, char** argv) { - QApplication app(argc, argv); - - QRhiWindow::InitParams initParams; - initParams.backend = QRhi::D3D12; - QRenderWidget widget(initParams); - - auto camera = widget.setupCamera(); - camera->setPosition(QVector3D(20, 15, 12)); - camera->setRotation(QVector3D(-30, 145, 0)); - - auto staticMesh = QStaticMesh::CreateFromFile(RESOURCE_DIR"/Model/mandalorian_ship/scene.gltf"); - - widget.resize({ 800,600 }); - widget.show(); - - return app.exec(); + QRhiD3D11InitParams params; + QRhi::Flags flags; + QSharedPointer rhi(QRhi::create(QRhi::D3D11, ¶ms, flags)); + qDebug() << rhi->driverInfo(); //打印显卡设备信息 + return 0; } \ No newline at end of file diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt deleted file mode 100644 index 20e58aaa..00000000 --- a/Source/CMakeLists.txt +++ /dev/null @@ -1,53 +0,0 @@ -cmake_minimum_required(VERSION 3.12) - -project(ModernGraphicsEngineGuide CXX) - -option(QENGINE_BUILD_RELEASE "Build Release" FALSE) - -set(RELEASE_OUTPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../Release) - -function(add_example EXAMPLE_PATH) - get_filename_component(EXAMPLE_NAME ${EXAMPLE_PATH} NAME) - file(GLOB_RECURSE PROJECT_SOURCE FILES ${EXAMPLE_PATH}/*.h ${EXAMPLE_PATH}/*.cpp ${EXAMPLE_PATH}/*.qrc) - add_executable(${EXAMPLE_NAME} - ${PROJECT_SOURCE} - ) - target_link_libraries(${EXAMPLE_NAME} PRIVATE QEngineUtilities) -endfunction() - -function(add_example_dir_internal DIR_PATH GROUP_NAME) - file(GLOB EXAMPLE_LIST RELATIVE ${DIR_PATH} ${DIR_PATH}/*) - foreach(EXAMPLE_NAME ${EXAMPLE_LIST}) - if(NOT EXISTS "${DIR_PATH}/${EXAMPLE_NAME}/Source") - add_example_dir_internal(${DIR_PATH}/${EXAMPLE_NAME} ${GROUP_NAME}/${EXAMPLE_NAME}) - elseif(IS_DIRECTORY ${DIR_PATH}/${EXAMPLE_NAME}) - add_example(${DIR_PATH}/${EXAMPLE_NAME}) - set_target_properties(${EXAMPLE_NAME} PROPERTIES FOLDER ${GROUP_NAME}) - if(QENGINE_BUILD_RELEASE) - file(RELATIVE_PATH RESOURCE_DIR ${RELEASE_OUTPUT_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/Resources) - target_compile_definitions(${EXAMPLE_NAME} PRIVATE RESOURCE_DIR="${RESOURCE_DIR}") - set_target_properties(${EXAMPLE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${RELEASE_OUTPUT_DIR}$<0:>) - set_target_properties(${EXAMPLE_NAME} PROPERTIES OUTPUT_NAME ${GROUP_NAME}-${EXAMPLE_NAME}) - else() - target_compile_definitions(${EXAMPLE_NAME} PRIVATE RESOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/Resources") - endif() - endif() - endforeach() -endfunction() - -function(add_example_dir DIR_NAME) - add_example_dir_internal(${CMAKE_CURRENT_SOURCE_DIR}/${DIR_NAME} ${DIR_NAME}) -endfunction() - -set_property(GLOBAL PROPERTY USE_FOLDERS ON) -set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP "Generated Files") - -add_subdirectory(0-QEngineUtilities) - -set(CMAKE_MAP_IMPORTED_CONFIG_DEBUGEDITOR Debug Release) -find_package(Qt6 COMPONENTS Core Widgets Gui Multimedia ShaderTools REQUIRED) - -add_example_dir(1-GraphicsAPI) -add_example_dir(2-EngineTechnology) -add_example_dir(3-GraphicsTechnology) -set_property(TARGET 01-Editor PROPERTY AUTOMOC ON) diff --git a/Source/Resources/Shader/color.frag b/Source/Resources/Shader/color.frag deleted file mode 100644 index bb94a5a6..00000000 --- a/Source/Resources/Shader/color.frag +++ /dev/null @@ -1,15 +0,0 @@ -#version 440 - -layout(location = 0) in vec3 v_color; - -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf { - mat4 mvp; - float opacity; -} ubuf; - -void main() -{ - fragColor = vec4(v_color * ubuf.opacity, ubuf.opacity); -} diff --git a/mkdocs.yml b/mkdocs.yml index 7f2a01c9..48b99ceb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,29 +14,39 @@ nav: - '5 - GUI': '00-C++/6.GUI.md' - '图形API': - "0 - 概述" : "01-GraphicsAPI/0.概述.md" - - "1 - 渲染窗口" : "01-GraphicsAPI/1.渲染窗口.md" - - "2 - 着色器" : "" - - "3 - 缓冲区" : "" - - "4 - 纹理" : "" - - "5 - 混合与测试" : "" - - "6 - 实例化" : "" - - "7 - 多渲染目标" : "" - - "8 - 离屏渲染" : "" - - "9 - 间接渲染" : "" + - "1 - 基础" : "01-GraphicsAPI/1.基础.md" + - "2 - 图形渲染管线" : "01-GraphicsAPI/2.图形渲染管线.md" + - "3 - 着色器" : "01-GraphicsAPI/3.着色器.md" + - "4 - 缓冲区与纹理" : "01-GraphicsAPI/4.缓冲区与纹理.md" + - "5 - 3D空间" : "01-GraphicsAPI/5.三维空间.md" + - "6 - 图形API进阶" : "01-GraphicsAPI/6.图形渲染进阶.md" - '引擎技术': - - "0 - 渲染架构" : "02-EngineTechnology/Waitting.md" - - "1 - 编辑器" : "" - - "2 - DebugDraw" : "" - - "3 - GPU调试" : "" + - "0 - 渲染架构" : "02-EngineTechnology/0.渲染架构.md" + - "1 - 编辑器架构" : "02-EngineTechnology/1.编辑器架构.md" + - "2 - DebugDraw" : "02-EngineTechnology/Waitting.md" + - "3 - GPU调试" : "02-EngineTechnology/Waitting.md" - '图形技术': - '0 - 网格体渲染': "03-GraphicsTechnology/Waitting.md" - - '1 - 天空盒': "" - - '2 - GPU粒子': "" - - '3 - 后期滤镜': "" - - '4 - 光照': "" - - '5 - 体积雾': "" - - '6 - 天空大气': "" - - '7 - 地形': "" + - '1 - 天空盒': "03-GraphicsTechnology/Waitting.md" + - '2 - GPU粒子': "03-GraphicsTechnology/Waitting.md" + - '3 - 后期滤镜': "03-GraphicsTechnology/Waitting.md" + - '4 - 光照': "03-GraphicsTechnology/Waitting.md" + - '5 - 体积雾': "03-GraphicsTechnology/Waitting.md" + - '6 - 天空大气': "03-GraphicsTechnology/Waitting.md" + - '7 - 地形': "03-GraphicsTechnology/Waitting.md" + - 'Unreal Engine': + - '0 - 基础编程': '04-UnrealEngine/0.基础编程.md' + - '1 - Slate 开发': '04-UnrealEngine/1.Slate开发.md' + - '2 - 插件开发': '04-UnrealEngine/2.插件开发.md' + - '3 - 粒子系统': '04-UnrealEngine/3.粒子系统.md' + - '4 - Niagara 性能优化': '04-UnrealEngine/4.Niagra性能优化.md' + - '5 - Fluid Ninja 流体插件': '04-UnrealEngine/5.FluidNinja流体插件.md' + - '6 - 开放世界制作': '04-UnrealEngine/6.开放世界制作.md' + - '7 - 音频开发': '04-UnrealEngine/7.音频开发.md' + - '9 - 植被优化技术': '04-UnrealEngine/9.开放世界中的植被优化技术.md' + - 'Other': + - '0 - 开放世界杂谈': '05-Other/0.开放世界杂谈.md' + - '1 - 元宇宙杂谈': '05-Other/1.元宇宙杂谈.md' theme: icon: repo: fontawesome/brands/github @@ -85,6 +95,9 @@ plugins: docs_path: Docs enabled: true extra: + analytics: + provider: google + property: G-S1J8MN27P8 social: - icon: fontawesome/brands/github link: https://github.com/Italink