虚幻引擎是如何渲染一帧的
虚幻引擎、HDR 渲染管线、WebGL1、Cesium
目录
目的:了解 UE 中比较重要的渲染 passes,作为一个 HDR 渲染管线的参考,并在 Web 端复现。
场景
用 Cesium For Unreal 插件新建一个场景,包含:
- 一个 CesiumSkyLight,其内包含:
- 一个 Directional Light 组件
- 一个 SkyLight 组件
- 一个 SkyAtmosphere 组件
- CesiumTerrain
- 2 个 UE 自带的人物模型,其中一个模型的 Shading Model 为 Clear Coat,另一个为 Default Lit
- 一个半透明的球体
- 一系列金属度和粗糙度不同的球体
- 一个 Default Lit 的立方体
在UE 编辑器内的渲染效果:

总览
用 Render Doc 插件捕获一帧:

- SkyAtmosphereLUTs:计算大气的 LUTs
- CaptureConvolveSkyEnvMap:实时捕获天空的环境光贴图(SkyLight 组件),并计算对应的漫反射球谐函数系数
- BasePass:渲染所有不透明物体的信息到 GBuffer 中
- ShadowDepths:对场景内每盏 cast shadow 的灯,渲染所有物体的深度到 Shadow Map Atlas 中
- LightCompositionTasks_PreLighting:计算 SSAO,用于 Lights 阶段
- Lights:使用 GBuffer、Shadow Map Atlas、SSAO 计算直接光照。此时阴影内的像素值应该为(0,0,0)
- ReflectionIndirect:在直接光照的基础上叠加间接光照,包括 SSR、平面反射和天空光 IBL,此时阴影内的像素应该不为(0,0,0)
- SkyAtmosphere:在未被非透明物体覆盖的像素上渲染大气,在被非透明物体覆盖的像素上应用空气透视
- Translucency:叠加半透明物体
- PostProcessing:最后的后处理部分,包括 TSR 抗锯齿、自动曝光、bloom 和 tone mapping
SkyAtmosphereLUTs
首先是计算和大气渲染相关的 look up tables。为了后续大气的渲染,我们首先需要计算一些 LUTs。

这些 LUTs 大概是这样的:






其中:
- Transmittance、Multi-Scattering 和 Sky View 用于后续中大气的渲染
- Distant Sky Light 是一个 1x1 分辨率的纹理,保存了大气在 6km 处的平均颜色,用于体积云的渲染
- Real Time Capture Sky View 用于天空光 Cube Map 的快速生成
- Camera Volume 是一个 3D 纹理(对不支持 3D 纹理的 API 则渲染到 2D 纹理上),用于不透明物体的空气透视
UE 的大气渲染总结到一篇 paper 中 [1] ,具体的实现还得结合对应的 UE 源码查看。
CaptureConvolveSkyEnvMap
这一 pass 会捕获天空(大气和体积云)的 Cube Map。为了分摊渲染的开销,且场景内短时间的变化,天空的视觉效果不会发生很大的变化,所以会使用时间切片(time slice)的方法来渲染这个 Cube Map。
默认情况下是开启时间分片的,可以使用命令 r.SkyLight.RealTimeReflectionCapture.TimeSlice 0
关闭。如果使用时间切片,则每 12 帧才会完成一次完整的计算,1 帧只完成其中的一个步骤,否则会在一帧内完成所有的计算。
关闭时间切片时,调用的图形 API 如下:

这 12 个步骤分别是:
- 渲染大气到
Captured
SkyRenderTarget
,对应图中的 6 个 Draw(3) - 渲染体积云(如果场景内有的话)到
Captured
SkyRenderTarget
,对应图中的 6 个 Draw(3) - 生成
Captured
SkyRenderTarget
的所有 mipmaps,对应图中的所以 MipGen。因为默认情况下,这个立方体贴图的分辨率是 128,所以需要生成除了 0 级之外的其它 7 个 level - 对
Captured
SkyRenderTarget
的 mip level 0 的 face 0 和 1 进行卷积,渲染到Convolved
SkyRenderTarget
对应的 mip level 的 faces 中,对应图中的第 1 个 Convolve - 对 CapturedSkyRenderTarget 的 mip level 0 的 face 2 和 3 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 的 faces中,对应图中的第 1 个 Convolve
- 对 CapturedSkyRenderTarget 的 mip level 0 的 face 4 和 5 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 的 faces 中,对应图中的第 1 个 Convolve
- 对 CapturedSkyRenderTarget 的 mip level 1 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 2 个 Convolve
- 对 CapturedSkyRenderTarget 的 mip level 2 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 3 个 Convolve
- 对 CapturedSkyRenderTarget 的 mip level 3 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 4 个 Convolve
- 对 CapturedSkyRenderTarget 的 mip level 4 和 5 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 5 个和第 6 个 Convolve
- 对 CapturedSkyRenderTarget 的 mip level 6 及之后 levels 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 7 个和第 8 个 Convolve
- 根据
Convolved
SkyRenderTarget
计算对应的 SH 系数,它是 7 个float4
的数据,对应图中的 ComputeSkyEnvMapDiffuseIrradianceCS
其中所谓的卷积即过滤,为的是避免使用高频的 cube map 生成的 SH 系数,然后再用这些 SH 系数插值出来的 diffuse irradiance cube map 可能会存在 "ringing" 问题 [2] 。
经过这一 pass 后,我们会得到 2 个有用的数据,用于 IBL 镜面反射的 Convolved
SkyRenderTarget
:

和由 7 个 float4
组成的用于 IBL 漫反射的 SH 系数。对应的 UE 代码位于 ReflectionEnvironmentRealTimeCapture.cpp 文件
BasePass
这一 pass 会渲染场景内所以不透明物体的 GBuffer 和深度值。

渲染过程如下:
RenderTargets 的信息如下:
- RT0(SceneColor):RGB 存 emission color * preExposure
- RT1(SelectionOutlineColor):RGB 通道保存世界坐标的法线,alpha 通道保存了 GBuffer.PerObjectGBufferData
- RT2(GBufferB):RGB 通道分别保存 metallic、specular 和 roughness,alpha 通道保存了
GBuffer.ShadingModelID | GBuffer.SelectiveOutputMask
- RT3(GBufferC):RGB 通道保存了 base color,alpha 通道保存了
GBuffer.IndirectIrradiance * GBuffer.GBufferAO
ShadowDepths
这一 pass 会渲染 shadow map,为每盏 cast shadow 的灯渲染 Shadow Map。

对于方向光,则计算 3 个距离的 cascade 到一个 shadow map atlas 中,例如当场景内有一个 Directional Light 充当太阳光时,渲染过程大概是这样的:
LightCompositionTasks_PreLighting
这一 pass 会计算屏幕空间的 AO。

- AmbientOcclusionSetup 618x319:首先是计算半宽高分辨率的法线
- AmbientOcclusionPS 618x319:然后是计算半宽高分辨率的 AO
- AmbientOcclusionPS 1236x638:最后把半宽高的 AO 上采样到全分辨率
Lights
这一 pass 是计算直接光照。

用每盏灯的信息、GBuffer、SSAO、Shadow Map Atlas

在着色器内根据 Shading Model 使用不同的方法计算每个像素的直接光照,对应的 UE 着色器代码是 DeferredLightPixelShaders.usf。对于 Default Lit,基本计算公式就是:
$$ (L \cdot \cos{(\theta)} \cdot \text{BRDF}) * \text{shadow} $$
因为乘上了阴影,所以此时阴影内的像素是全黑的。直接光照的渲染结果:

ReflectionIndirect
这一 pass 分为 2 部分,首先计算 SSR(屏幕空间反射,或称为屏幕空间光线追踪),然后再应用 (SSR + 平面反射 + 天空光 convolved cube map) 作为镜面反射和 SH 系数重建的 irradiance cube map 作为漫反射。

SSR 纹理的生成使用的是上一帧的渲染结果,生成的 SSR 纹理:

然后是对 Lights pass 的直接光照渲染结果叠加上 SSR 和 CaptureConvolveSkyEnvMap pass 生成的天空光 IBL:

和直接光照相比,此时阴影内的像素不再是黑色的了,对应的着色器代码位于 ReflectionEnvironmentPixelShader.usf 内。
SkyAtmosphere
这一 pass 会将大气渲染到没有物体覆盖的像素(即对应的深度值为最远处的深度值,例如 1.0),且对有不透明物体覆盖的像素(即对应的深度值不为最远处的值)应用空气透视。

这一 pass 会使用到 SkyAtmosphereLUTs pass 生成的 LUTs,最后结果为:

Translucency
这一 pass 会渲染半透明物体。

结果:

PostProcessing
至此,已经得到了场景的 HDR 的渲染结果,现在需要进行最后的后处理部分,对应的文件是 PostProcessing.cpp。

TemporalSuperResolution
这一 pass 会对场景的 HDR 渲染结果应用时间上地超分辨率抗锯齿。
Downsample
这一 pass 会对应用过抗锯齿的渲染结果进行降采样,降低为 1/2 分辨率,作为后处理的输入。
SceneDownsample
这一 pass 会对 1/2 分辨率的渲染结果进行降采样,结果为 1/4、1/8、1/16、1/32 和 1/64 分辨率的渲染结果。
BasicEyeAdaptation
UE 的自动曝光方式有“Histogram”和“Basic”这两种方式,因为“Histogram”的方式需要 compute shader,为了能在用 WebGL 复现,我们选择的是“Basic”的自动曝光方式。
“Basic”方式的主要思路是对降采样的最后一个纹理(即 1/64 分辨率)进行遍历,采样每个纹素值,然后根据这些纹素值计算它们的平均亮度和曝光值,并把计算出来的信息保存到一个 1x1 的纹理中。
Bloom
这一 pass 会计算泛光纹理。泛光纹理的计算使用的是 6 层的高斯过滤。
最后计算出来的泛光纹理:

CombineLUTS
这一 pass 主要是计算一个用于 tone map 的 32x32x32 LUT。这个 LUT 会根据后处理的设置来生成,参数包含 film tone mapping 和用户自定义的调色。这个 LUT 大概张这样:

Tonemap
最后一 pass 是 tone mapping。

输入的纹理是 BasicEyeAdaptation 生成的 1x1 纹理、TSR 生成的渲染结果、泛光纹理和 CombineLUTS 生成的 LUT。最终的输出就是显示在显示器上的渲染内容。
Web 端的复现
下面是在 Web 端用 Cesium.js/WebGL1 对这整套渲染管线的复现效果:



效果和 UE 相差不大,当然,还有一些是未复现的,包括:
- TSR 抗锯齿
- SSR
- SSAO
- 阴影
- 体积云