多线程模型
默认情况下,UE存在 Game Thread、Render Thread、RHI Thread,它们都独立地运行在专门的线程上(FRunnableThread)。
以场景中的一盏灯(ULightComponent)为例,它拥有完整的属性,可以被游戏逻辑随时修改(改变颜色、亮度)。
- 当灯光被创建或其属性发生变化时,游戏线程会创建一个 RenderProxy(如 FLightSceneProxy) 用于渲染线程使用。这个Proxy包含了渲染所需的数据(位置、颜色、强度等)的当前快照。
- 游戏线程将这个Proxy(或者更新命令)通过线程安全的命令列表(ENQUEUE_RENDER_COMMAND)发送给渲染线程。
- 渲染线程通常在下一帧使用这个Proxy进行渲染计算。渲染线程作为前端(frontend)产生的Command List是平台无关的,是抽象的图形API调用。
- 渲染线程将渲染命令添加到RHICommandList。RHI线程不断取出指令,向GPU发送,并阻塞等待结果。此时RHI线程虽然阻塞,但是渲染线程依然正常工作,可以继续处理向RHI命令列表 填充指令。RHI线程作为后端(backtend)会执行和转换渲染线程的Command List成为指定图形API的调用(称为Graphical Command),并提交到GPU执行。
这些线程处理的数据通常是不同帧的,譬如游戏线程处理N帧数据,渲染线程和RHI线程处理N-1帧数据:

但也存在例外,比如渲染线程和RHI线程运行很快,几乎不存在延迟,这种情况下,游戏线程处理N帧,而渲染线程可能处理N或N-1帧,RHI线程也可能在转换N或N-1帧。但是,渲染线程不能落后游戏线程一帧,否则游戏线程会卡住,直到渲染线程处理所有指令。
游戏线程和渲染线程代表
游戏线程的对象通常做逻辑更新,在内存中有一份持久的数据,为了避免游戏线程和渲染线程产生竞争条件,会在渲染线程额外存储一份内存拷贝,并且使用的是另外的类型,以下是UE比较常见的类型映射关系(游戏线程对象以U开头,渲染线程以F开头):
| Game Thread | Rendering Thread |
|---|---|
| UWorld | FScene |
| UPrimitiveComponent | FPrimitiveSceneProxy / FPrimitiveSceneInfo |
| - | FSceneView / FViewInfo |
| ULocalPlayer | FSceneViewState |
| ULightComponent | FLightSceneProxy / FLightSceneInfo |
游戏线程代表一般由游戏游戏线程操作,渲染线程代表主要由渲染线程操作。如果尝试跨线程操作数据,将会引发不可预料的结果,产生竞争条件。
| |
部分代表比较特殊,如FPrimitiveSceneProxy、FLightSceneProxy ,这些场景代理本属于引擎模块,但又属于渲染线程专属对象,说明它们是连接游戏线程和渲染线程的桥梁,是线程间传递数据的工具人。
渲染一帧流程
- CPU第1帧开始,可见性剔除
- Early Z Pass(Depth only pass,和硬件的 Early Z 优化区分开),创建的第一个缓存区,叫“深度通道”,意味着我们可以在大多数材质的材质编辑器中访问场景深度
- Defer 渲染,生成 G Buffer,包括基础颜色、高光度、粗糙度、金属感,世界法线等等。这里如果场景中有静态光照,并且由Lightmass生成,那么这里还会用到因此创建的光照贴图
- 计算光照和阴影
- 雾和大气特效计算
- 半透明对象绘制
- 后处理
参考
https://zhuanlan.zhihu.com/p/546968578
https://zhuanlan.zhihu.com/p/1983913933166354972
https://zhuanlan.zhihu.com/p/21291425593
https://www.bilibili.com/video/BV1VT421a7vw
《大象无形》