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

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:计算大气的 LUTs
  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。为了后续大气的渲染,我们首先需要计算一些 LUTs。

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 会捕获天空(大气和体积云)的 Cube Map。为了分摊渲染的开销,且场景内短时间的变化,天空的视觉效果不会发生很大的变化,所以会使用时间切片(time slice)的方法来渲染这个 Cube Map。

默认情况下是开启时间分片的,可以使用命令 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。因为默认情况下,这个立方体贴图的分辨率是 128,所以需要生成除了 0 级之外的其它 7 个 level
  4. CapturedSkyRenderTargetmip level 0 的 face 0 和 1 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 的 faces 中,对应图中的第 1 个 Convolve
  5. 对 CapturedSkyRenderTarget 的 mip level 0 的 face 2 和 3 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 的 faces中,对应图中的第 1 个 Convolve
  6. 对 CapturedSkyRenderTarget 的 mip level 0 的 face 4 和 5 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 的 faces 中,对应图中的第 1 个 Convolve
  7. 对 CapturedSkyRenderTarget 的 mip level 1 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 2 个 Convolve
  8. 对 CapturedSkyRenderTarget 的 mip level 2 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 3 个 Convolve
  9. 对 CapturedSkyRenderTarget 的 mip level 3 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 4 个 Convolve
  10. 对 CapturedSkyRenderTarget 的 mip level 4 和 5 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 5 个和第 6 个 Convolve
  11. 对 CapturedSkyRenderTarget 的 mip level 6 及之后 levels 的所有 faces 进行卷积,渲染到 ConvolvedSkyRenderTarget 对应的 mip level 中,对应图中的第 7 个和第 8 个 Convolve
  12. 根据 ConvolvedSkyRenderTarget 计算对应的 SH 系数,它是 7 个 float4 的数据,对应图中的 ComputeSkyEnvMapDiffuseIrradianceCS

其中所谓的卷积即过滤,为的是避免使用高频的 cube map 生成的 SH 系数,然后再用这些 SH 系数插值出来的 diffuse irradiance cube map 可能会存在 "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):RGB 存 emission color * preExposure
  2. RT1(SelectionOutlineColor):RGB 通道保存世界坐标的法线,alpha 通道保存了 GBuffer.PerObjectGBufferData
  3. RT2(GBufferB):RGB 通道分别保存 metallic、specular 和 roughness,alpha 通道保存了 GBuffer.ShadingModelID | GBuffer.SelectiveOutputMask
  4. RT3(GBufferC):RGB 通道保存了 base color,alpha 通道保存了 GBuffer.IndirectIrradiance * GBuffer.GBufferAO

ShadowDepths

这一 pass 会渲染 shadow map,为每盏 cast shadow 的灯渲染 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 上采样到全分辨率
    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} $$

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

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 会对场景的 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 层的高斯过滤。

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

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。

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

输入的纹理是 BasicEyeAdaptation 生成的 1x1 纹理、TSR 生成的渲染结果、泛光纹理和 CombineLUTS 生成的 LUT。最终的输出就是显示在显示器上的渲染内容。

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 评论加载中...