当前位置: 首页 > news >正文

Unreal渲染源码简读(一)RHI/Shader

RHI作为Unreal的一个跨平台渲染封装, 存在于上层显示和下层图形api之间, 即一个中间接口层。本篇内容将通过draw call的执行过程,认识到Unreal的封装方法,以及shader与Fshader的具体应用。

一、认识draw call的执行过程

1.1复习渲染管线

首先一起来复习一下渲染管线的知识。如图1.1.1,是GPU的渲染管线执行流程。

图片

图1.1.1 渲染管线执行流程

一个draw call的产生,由以上的流程所生成的。这是一个通用的排布, 我们也可以根据所需进行局部调整。

在这个管线执行过程前, 即在cpu给gpu发送指令前,除了具体的数据外,传入的参数的数据格式,也非常重要, 下面会提及。

1.2以d3d的角度理解管线装配过程

我们以龙书里的d3d代码段为案例,去理解管线装配的过程。

d3d提供一个pipelineState,用来创建管线的状态配置。如果要执行不同的shader代码,就要用不同的配置,需要创建不同的pipelineState。即告诉api,我们要用哪个配置项,执行哪个shader。

图片

图1.2.1 管线的状态设置

我们可以看到,是先申请内存,然后再去填充各种状态。其中.VS和.PS就是把编译好的shader代码放进去,这里通过指针和size给到对应的指定。

而pRootSignature就是规定好代码的输入参数数据格式,如下图1.2.2所示。

图片

图1.2.2 pRootSignature的输入参数数据格式

构造完成之后, 就用CD3DX12_ROOT_SIGNATURE_DESC打包成根签名描述, 再用它去创建一个真正的根签名,即上面的mRootSignature。

东西准备完了那就要draw了。从图1.2.3中可以看到,之前创建的mOpaquePSO和 mRootSignature就被放进去了。

图片

图1.2.3 渲染执行

渲染管线状态和根签名传完以后,那么就可以传入真正的参数了,本质上就是拿虚拟地址,给到显存映射好。

图片

图1.2.4 渲染内容设置

另外,IASetVertexBuffers传入顶点属性,SetGraphicsRootDescriptorTable传入贴图等实际内容填充,都在这里执行。在搞定所有内容之后,只需要再执行 DrawIndexedInstanced就可以让gpu执行了。

图片

图1.2.5 渲染内容填充

二、Unreal的封装方法

接下来我们回归本篇内容的核心——在Unreal中要怎么去封装上述D3D以及其他API的渲染过程 (限于篇幅这里只提及部分内容封装)

2.1资源封装

我们先从资源开始,所有参数的设置状态, 本质都是一段内存或者显存, 就是d3d的resource, 或者OpenGL的无符号整数resource id。

Unreal提供了一个FRHIResource , 实际内容可以是uniform buffer,也可以是texture,类型可以通过它的ERHIResourceType 枚举变量查看到, 如图2.1.1。

图片

图2.1.1 资源类型

这个FRHIResource基类里没定义太多的操作,只有有AddRef,Release和原子操作。

我们往子类看,来到FRHIUniformBuffer,这里的封装是出于一个与平台无关的的状态,只是定义了对外的接口。接下来我们就来具体讲解它的使用方法。

图片

图2.1.3 FRHIUniformBuffer

接下来我们再子类看, 来到FD3D12UniformBuffer。

图片

图2.1.4 FD3D12UniformBuffer 

来到熟悉的FD3D12ResourceLocation,我们进去看看。

图片

图2.1.5 FD3D12ResourceLocation里的内容

可以看到有FD3D12Resource变量。

图片

图2.1.6 FD3D12Resource

最终找到了d3d12里真正存储resource的ID3D12Resource指针。这个location里面有一个成员变量,又是一个FD3D12Resource。而这个FD3D12Resource里面,就是真正D3D12的指针。当然,下面还有一些别的被它封装了起来,比如GPUVirtualAddress。

所以这就是一套简单的继承体系,我们想用什么接口,就在RHI层面定义一个接口,在子类里去实现它。

接口就是通用的, 比如Create、Release一段资源,或者Upload一段内存进去。

同理OpenGL那边也就会有一个FOpenGLUniformBuffer。

图片

图2.1.7 FOpenGLUniformBuffer

UE RHI只做一些命名和通用接口封装, 实现都是在不同的子类, 编译到哪种平台就用对应的上层逻辑代码, 只需要关注操作的是RHI层的东西就可以, 直接调用已经封装好的接口就行。

接下来我们以Unreal里最常用的UTexture2D举个例子:

图片

图2.1.8 UTexture2D

比如让它执行UpdateTextureRegions, 它实际上就是去调RHI的函数。

图片

图2.1.9 UpdateTextureRegions

图片

图2.1.10 执行更新

2.2执行封装

这里就不得不提到这里出现的FDynamicRHI了。FDynamicRHI是RHI的动态实现部分,而RHI是更底层的静态接口。这个类里的方法超多,比如常见的图形渲染操作,创建纹理、状态、更新资源、设置Fence、更新贴图等一大半操作,都在此类中。但都没实现。OpenGL、D3D会去继承它、实现它。所以不要在上层写api的实际调用内容,否则代码无法跨平台。

这里面都是些纯虚函数,同样下面有D3D和OpenGL的各种各样的子类。

图片

图2.2.1 FDynamicRHI

三、在Unreal中封装

3.1shader的封装

FRHI shader是继承FRHIResource, 思路也跟之前一样。其子类D3D和OpenGL会有具体且复杂的实现。

图片

图3.1.1 FRHI shader

成员Frequency枚举,用于定义该Shader是哪个类型。

图片

图3.1.2 成员Frequency枚举

以FRHIVertexShader为例我们来看一下, 先直接看d3d怎么做的吧。

图片

图3.1.3 FRHIVertexShader

这里继承两个类, 后面的FD3DShaderData才是主要内容。

图片

图3.1.4 FD3DShaderData

比较重要的就是code了, 内部将其转换成D3D的bytecode。

根据代码去创建一个shader的话, 就要用到之前提到的FDynamicRHI定义的接口,比如OpenGL的实现如下。

图片

图片

图3.1.5 创建shader

进入后,先会解析一下Code,因为Unreal这里的code不是纯代码,还有带有参数,所以会先用一个Reader解析。

图片

图3.1.6 获取Optional数据大小

图片

图3.1.7 获取实际Code大小

可以看到Code前半段是代码,后半段是Optional的数据。代码长度=总长-OptionalData长度。

解析完代码以后, 会进行一个glsl的转换, 因为OpenGL在各个平台也有适配差异, 是需要做一些适配变化的。

图片

图3.1.8 glsl转换

最终传入代码, 并进行编译操作。

图片

图3.1.9 输入并编译代码

d3d同理, 这里就不做阐述了。

3.2FShader简介

FShader是上层的一个类,FRHIShader相对于自己那套继承体系是上层,但相对于FShader则是底层。有RHI的都是相对的底层或者中间层。

图片

图3.2.1 shader创建

FGlobalShader可能大家比较熟悉,自己要写了一个hlsl的shader,那通常继承FGlobalShader, 而 FGlobalShader是继承自FShader。

图片

图3.2.2 Unreal官方hlsl转glsl shader pipeline的示意图

所以我们写的usf并不是最终的代码,还会继续进行转换。转换过程中,就会去检测编译选项,一个usf中有不同的各种各样不同的编译选项,即排列组合permutation。根据不同的排列组合编译选项,去编译不同的shader,虽然看起来是同一份shader代码。这个转换过程十分复杂,不必深究,我们知道如果出现permutation之类的编译选项,则同一份shader代码可能编译出不同的shader即可。

3.3FShader怎么做反射

FShader没有像Unreal其他UObject类的反射,用UPROPERTY就拿到它的反射,而是通过DECLARE_TYPE_LAYOUT宏,单独实现的一套反射机制。

图片

图3.3.1 反射宏

比如这里FShader的成员变量, 只要加了这个LAYOUT_XXX,就有反射信息。它是怎么做到的呢, 我们展开一个LAYOUT_FIELD 宏。

图片

图3.3.2 宏展开

首先定义Bindings成员变量本身,然后定义了一个新的模板类InternalLinkType,即记录类型, 和用offsetof拿到偏移量。

最终达到的目的是:只要拿到FShader的指针,加上这个Offset后,就拿到了这个Bindings成员变量的地址,再进行到FShaderParameterBindings的内存转换,就能拿到这段内存了。

LAYOUT_FIELD 的信息收集,就是收集Name、收集成员变量的指针、收集其他信息。

总结:本篇内容介绍了Unreal 的封装方法,讲解了在Unreal中如何实现shader的封装以及Fshader的反射。如果想了解更多关于封装调用,形成完整渲染pass的信息,欢迎发送邮件至mkt@eptcom.com联系我们。

相关文章:

  • Minecraft Fabric - java.lang.NoClassDefFoundError HttpUriRequest
  • Spring Boot是什么?MybatisPlus常用注解,LambdaQueryWrapper常用方法
  • OpenHarmony 4.1版本应用升级到5.0版本问题记录及解决方案
  • vue开发中常用方法笔记
  • 在公司快速查看与固定内网IP地址的完整指南
  • 全链路解析:影刀RPA+Coze API自动化工作流实战指南
  • 2025电工杯A题数据-光伏电站发电功率预测数据 收集策略
  • Docker 与 Kubernetes 部署 RabbitMQ 集群(二)
  • docker初学
  • w~大模型~合集4
  • 【Linux系列】EVS 与 VBD 的对比
  • nvidia Thor U与qualcomm 8295 DMPIS算力测试对比
  • 用matlab提取abaqus odb文件中的节点信息
  • 谢飞机的Spring WebFlux面试之旅:从基础到深入
  • 服务接口鉴权与内部认证:自定义注解与AOP实现的企业级实践
  • 免杀一 线程加载
  • Excel 打开密码:守护数据安全的 “钥匙”
  • MySQL:备份还原数据库(mysqldump)
  • c# 解码 encodeURIComponent
  • RocketMQ 生产消费消息消息解析与重试机制详解
  • 宁波公司网站建立/北京seo优化哪家公司好
  • 网站及单位网站建设情况/百度一下图片识别
  • 做软件赚钱的网站有哪些/友情链接管理系统
  • 旅游网站后台html模板/站长之家网站介绍
  • 群晖nas安装wordpress安装/seo排名教程
  • 制作app软件的公司/seo外包公司一般费用是多少