虚幻引擎是如何渲染一帧的

Venus

虚幻引擎、HDR 渲染管线、WebGL1、Cesium

目录

  1. 场景
  2. 总览
  3. SkyAtmosphereLUTs
  4. CaptureConvolveSkyEnvMap
  5. BasePass
  6. ShadowDepths
  7. LightCompositionTasks_PreLighting
  8. Lights
  9. ReflectionIndirect
  10. SkyAtmosphere
  11. Translucency
  12. PostProcessing
    1. TemporalSuperResolution
    2. Downsample
    3. SceneDownsample
    4. BasicEyeAdaptation
    5. Bloom
    6. CombineLUTS
    7. Tonemap
  13. Web 端的复现

目的:了解 UE 中比较重要的渲染 passes,作为一个 HDR 渲染管线的参考,并在 Web 端复现。

场景

用 Cesium For Unreal 插件新建一个场景,包含:

  1. 一个 CesiumSkyLight,其内包含:
    1. 一个 Directional Light 组件
    2. 一个 SkyLight 组件
    3. 一个 SkyAtmosphere 组件
  2. CesiumTerrain
  3. 2 个 UE 自带的人物模型,其中一个模型的 Shading Model 为 Clear Coat,另一个为 Default Lit
  4. 一个半透明的球体
  5. 一系列金属度和粗糙度不同的球体
  6. 一个 Default Lit 的立方体

在UE 编辑器内的渲染效果:

assets/images/how-unreal-engine-renders-a-frame/29.png

总览

用 Render Doc 插件捕获一帧:

assets/images/how-unreal-engine-renders-a-frame/overview.png
我们主要看和渲染相关的红框部分,分别有:

  1. SkyAtmosphereLUTs:计算大气的 look up tables
  2. CaptureConvolveSkyEnvMap:实时捕获天空的环境光贴图(SkyLight 组件),并计算对应的低频漫反射部分,拟合为球谐函数系数
  3. BasePass:渲染所有不透明物体的信息到 GBuffer 中
  4. ShadowDepths:对场景内每盏勾选了 cast shadow 的灯渲染所有不透明物体的深度到 Shadow Map Atlas 中
  5. LightCompositionTasks_PreLighting:计算 SSAO,用于下一个 Lights 阶段
  6. Lights:使用 GBuffer、Shadow Map Atlas、SSAO 计算直接光照,此时阴影内的像素值应该为(0,0,0)
  7. ReflectionIndirect:在直接光照的基础上叠加间接光照,包括 SSR、平面反射和天空光 IBL,此时阴影内的像素应该不为(0,0,0)
  8. SkyAtmosphere:在未被物体覆盖的像素上渲染大气,和在被物体覆盖的像素上应用空气透视
  9. Translucency:渲染半透明物体到单独的纹理中
  10. PostProcessing:最后的后处理部分,包括 TSR 抗锯齿、自动曝光、bloom 和 tone mapping

SkyAtmosphereLUTs

首先是计算和大气渲染相关的 look up tables,用于后续的大气和空气透视渲染

assets/images/how-unreal-engine-renders-a-frame/skyatmosphereluts.png

这些 LUTs 大概是这样的:

assets/images/how-unreal-engine-renders-a-frame/skyatmosphere-transmittance-lut.png
Transmittance LUT
assets/images/how-unreal-engine-renders-a-frame/skyatmosphere-multiscattering-lut.png
Multi-Scattering LUT
assets/images/how-unreal-engine-renders-a-frame/skyatmosphere-distantskylight-lut.png
Distant Sky Light LUT
assets/images/how-unreal-engine-renders-a-frame/skyatmosphere-realtimecaptureskyview-lut.png
Real Time Capture Sky View LUT
assets/images/how-unreal-engine-renders-a-frame/skyatmosphere-skyview-lut.png
Sky View LUT
assets/images/how-unreal-engine-renders-a-frame/skyatmosphere-cameravolumelut-lut.png
Camera Volume LUT

其中:

  1. Transmittance、Multi-Scattering 和 Sky View 用于后续的大气渲染
  2. Distant Sky Light 是一个 1x1 分辨率的纹理,保存了大气在 6km 处的平均颜色,用于体积云的渲染
  3. Real Time Capture Sky View 用于天空光 Cube Map 的快速生成
  4. Camera Volume 是一个 3D 纹理(对不支持 3D 纹理的 API 则渲染到 2D 纹理上),用于不透明物体的空气透视

UE 的大气渲染总结到一篇 paper 中 [1] ,具体的实现还得结合对应的 UE 源码查看。

CaptureConvolveSkyEnvMap

这一 pass 会捕获天空(大气和体积云)作为场景物体的环境光贴图。因为场景内短时间的变化,天空的视觉效果不会发生很大的变化,所以为了分摊渲染的开销,会使用时间切片(time slice)的方法来渲染这个环境光贴图。

默认情况下是开启时间分片的,可以通过命令 r.SkyLight.RealTimeReflectionCapture.TimeSlice 0 关闭。如果使用时间切片,则每 12 帧才会完成一次完整的计算,1 帧只完成其中的一个步骤,否则会在一帧内完成所有的计算。

关闭时间切片时,调用的图形 API 如下:

assets/images/how-unreal-engine-renders-a-frame/captureconvolveskyenvmap.png
关闭时间切片时

这 12 个步骤分别是:

  1. 渲染大气到 CapturedSkyRenderTarget,对应图中的 6 个 Draw(3)
  2. 渲染体积云(如果场景内有的话)到 CapturedSkyRenderTarget,对应图中的 6 个 Draw(3)
  3. 计算 CapturedSkyRenderTarget 的所有 mipmaps,对应图中的所以 MipGen。因为默认情况下,这个立方体贴图的分辨率是 128x128,所以需要生成除了 0 级之外的其它 7 个 level
  4. CapturedSkyRenderTargetmip level 0 的 face 0 和 1 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 的 faces 中,对应图中的第 1 个 Convolve
  5. 同上,但是是对 mip level 0 的 face 2 和 3 进行卷积,对应图中的第 1 个 Convolve
  6. 同上,但是是对 mip level 0 的 face 4 和 5 进行卷积,对应图中的第 1 个 Convolve
  7. 同上,但是是对 mip level 1 的所有 faces 进行卷积,对应图中的第 2 个 Convolve
  8. 同上,但是是对 mip level 2 的所有 faces 进行卷积,对应图中的第 3 个 Convolve
  9. 同上,但是是对 mip level 3 的所有 faces 进行卷积,对应图中的第 4 个 Convolve
  10. 同上,但是是对 mip level 4 和 5 的所有 faces 进行卷积,对应图中的第 5 个和第 6 个 Convolve
  11. 同上,但是是对 mip level 6 及之后 levels 的所有 faces 进行卷积,对应图中的第 7 个和第 8 个 Convolve
  12. 通过卷积得到的 ConvolvedSkyRenderTarget,计算对应的球谐函数系数,它是 7 个 float4 的数据,对应图中的 ComputeSkyEnvMapDiffuseIrradianceCS

其中所谓的卷积即过滤,为的是避免使用高频的立方体贴图来生成 SH 系数,因为用这些 SH 系数插值出来的漫反射立方体贴图可能会存在 "ringing" 问题 [2]

经过这一 pass 后,我们会得到 2 个有用的数据,用于 IBL 镜面反射的 ConvolvedSkyRenderTarget

assets/images/how-unreal-engine-renders-a-frame/convolvedskyrendertarget.png
ConvolvedSkyRenderTarget

和由 7 个 float4 组成的用于 IBL 漫反射的 SH 系数。对应的 UE 代码位于 ReflectionEnvironmentRealTimeCapture.cpp 文件

BasePass

这一 pass 会渲染场景内所以不透明物体的 GBuffer 和深度值。

assets/images/how-unreal-engine-renders-a-frame/basepass.png

渲染过程如下:

RenderTargets 的信息如下:

  1. RT0(SceneColor, R16G16B16A16_FLOAT):rgb 通道保存 emission color * preExposure
  2. RT1(SelectionOutlineColor, R10G10B10A2_UNORM):rgb 通道保存世界坐标的法线,a 通道保存了 GBuffer.PerObjectGBufferData
  3. RT2(GBufferB, R8G8B8A8_UNORM):rgb 通道分别保存 Metallic、Specular 和 Roughness,a 通道保存了 GBuffer.ShadingModelID | GBuffer.SelectiveOutputMask
  4. RT3(GBufferC, R8G8B8A8_SRGB):rgb 通道保存了 base color,a 通道保存了 GBuffer.IndirectIrradiance * GBuffer.GBufferAO
  5. RT4(GBufferD, R8G8B8A8_UNORM):保存 GBuffer.CustomData
  6. RT5(GBufferE, R8G8B8A8_UNORM):保存 GBuffer.PrecomputedShadowFactor
  7. RT6(GBufferF):rgb 通道保存 GBuffer.WorldTangent,a 通道保存 GBuffer.Anisotropy
  8. Depth(D32S8):depth 值和 stencil 值

ShadowDepths

这一 pass 会渲染 shadow map。

assets/images/how-unreal-engine-renders-a-frame/shadowdepths.png
Shadow Dpeths

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

LightCompositionTasks_PreLighting

这一 pass 会根据法线计算屏幕空间的 AO。

assets/images/how-unreal-engine-renders-a-frame/lightcompositiontasks_prelighting.png

  1. AmbientOcclusionSetup 618x319:首先是计算半宽高分辨率的法线
  2. AmbientOcclusionPS 618x319:然后是计算半宽高分辨率的 AO
    assets/images/how-unreal-engine-renders-a-frame/ambientocclusionps_618x319.png
  3. AmbientOcclusionPS 1236x638:最后把半宽高的 AO 上采样到全分辨率(这里值全为 1,因为距离太远,所以不应用 SSAO?)
    assets/images/how-unreal-engine-renders-a-frame/ambientocclusionps_1236x638.png

Lights

这一 pass 会计算直接光照。

assets/images/how-unreal-engine-renders-a-frame/lights.png

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

assets/images/how-unreal-engine-renders-a-frame/lights_input.png
输入的纹理

在着色器内根据 Shading Model 使用不同的方法计算每个像素的直接光照,对应的 UE 着色器代码是 DeferredLightPixelShaders.usf。对于 Default Lit,基本计算公式就是:

$$ (L \cdot \cos{(\theta)} \cdot \text{BRDF}) * \text{shadow} $$

和 glTF 不同,在 BRDF 的计算中,除了金属度和粗糙度之外,UE 的 Default Lit 还使用了一个 Specular 值。

因为乘上了阴影,所以此时阴影内的像素是全黑的。直接光照的渲染结果:

assets/images/how-unreal-engine-renders-a-frame/lights_output.png

ReflectionIndirect

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

assets/images/how-unreal-engine-renders-a-frame/reflectionindirect.png

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

assets/images/how-unreal-engine-renders-a-frame/screenspacereflections.png

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

assets/images/how-unreal-engine-renders-a-frame/reflectionenvironmentandsky.png

和直接光照相比,此时阴影内的像素不再是黑色的了,对应的着色器代码位于 ReflectionEnvironmentPixelShader.usf 内。

SkyAtmosphere

这一 pass 会将大气渲染到没有物体覆盖的像素(即对应的深度值为最远处的深度值,例如 1.0),且对有物体覆盖的像素(即对应的深度值不为最远处的值)应用空气透视。

assets/images/how-unreal-engine-renders-a-frame/skyatmosphere.png

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

assets/images/how-unreal-engine-renders-a-frame/skyatmosphere_result.png

Translucency

这一 pass 会渲染半透明物体到单独的纹理中。

assets/images/how-unreal-engine-renders-a-frame/translucency.png

结果:

assets/images/how-unreal-engine-renders-a-frame/translucency_result.png

PostProcessing

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

assets/images/how-unreal-engine-renders-a-frame/postprocessing.png

TemporalSuperResolution

这一 pass 会对叠加半透明物体的纹理,并应用时间上地超分辨率抗锯齿(TSR),最终得到完整的渲染结果。

Downsample

这一 pass 会对之前的渲染结果进行降采样,宽高各降低为 $\frac{1}{2}$,作为后处理的输入。

SceneDownsample

这一 pass 会对 $\frac{1}{2}$ 宽高的渲染结果进行降采样,结果为 $\frac{1}{4}$、$\frac{1}{8}$、$\frac{1}{16}$、$\frac{1}{32}$ 和 $\frac{1}{64}$ 宽高的渲染结果。

BasicEyeAdaptation

UE 的自动曝光方式有“Histogram”“Basic”这两种方式,因为“Histogram”的方式需要 compute shader,为了能在用 WebGL 复现,我们选择的是“Basic”的自动曝光方式。

“Basic”方式的主要思路是对降采样的最后一个纹理(即 $\frac{1}{64}$ 宽高)进行遍历,采样每个纹素值,然后根据这些纹素值计算它们的平均亮度和曝光值,并把计算出来的信息保存到一个 1x1 纹理的 4 个分量中。

Bloom

这一 pass 会计算泛光纹理。泛光纹理的计算使用的是 6 层的高斯过滤。

最后计算出来的泛光纹理:

assets/images/how-unreal-engine-renders-a-frame/bloom.png

CombineLUTS

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

assets/images/how-unreal-engine-renders-a-frame/combineluts.jpg

Tonemap

最后一 pass 是 tone mapping。输入的纹理是 BasicEyeAdaptation pass 生成的 1x1 纹理、TSR pass 生成的渲染结果、Bloom pass 生成的泛光纹理和 CombineLUTS pass 生成的 LUT。最终的输出就是场景的渲染内容。

assets/images/how-unreal-engine-renders-a-frame/tonemap.png

最后还有一些和 UE 编辑器相关的渲染,例如选中模型时的黄色轮廓和模型的变换轴(gizmo)。

Web 端的复现

下面是在 Web 端用 Cesium.js/WebGL1 对这整套渲染管线的复现效果:

assets/images/how-unreal-engine-renders-a-frame/web.jpeg
assets/images/how-unreal-engine-renders-a-frame/web-noon.jpeg
assets/images/how-unreal-engine-renders-a-frame/web-noon.jpeg

效果和 UE 相差不大,当然,还有一些是未复现的,包括:

[1] Hillaire, Sébastien. (2020). A Scalable and Production Ready Sky and Atmosphere Rendering Technique. Computer Graphics Forum. 39. 13-22. 10.1111/cgf.14050.
[2] Sloan, Peter-Pike. (2008). Stupid Spherical Harmonics (SH) Tricks. Game Developers Conference.

Disqus 评论加载中...