【翻译】Motion Blur for mobile devices in Unity
原文链接:https://tech.spaceapegames.com/2018/09/06/motion-blur-for-mobile-devices-in-unity/
什么是运动模糊?-What is Motion Blur?
维基百科将运动模糊定义为:
运动模糊是在照片或序列帧中移动物体的明显拖尾,例如电影或动画。由于快速移动或长时间曝光,在记录单次曝光期间记录的图像发生变化时,会出现这种情况。
当我们使用相机拍摄图像时,快门打开,传感器捕获图像,然后快门再次关闭。快门打开的时间越长,传感器捕获的光线就越多。但是,将快门打开更长时间同时意味着拍摄的图像可能会改变。
想象一下,我们正尝试拍摄一辆沿着赛道飞驰的汽车图像。如果快门保持打开一秒,汽车会飞射出相机,并且整个图像将变得模糊。现在,如果我们将快门打开很短的时间,比如说1/500秒,那么我们可以拍摄到完全没有模糊的图像。
运动模糊是令快门长时间打开的一个副作用。在游戏中,可能需要模拟这种效果。它可以为我们的场景增添速度感和动感。根据游戏的类型,这可以帮助游戏提高真实感级别。可能从运动模糊效果中受益的游戏类型包括赛车游戏,第一人称射击游戏和第三人称射击游戏等。
管线概述-Pipeline Overview
我们希望为我们的一款赛车游戏开发运动模糊效果。目前有大量不同的实现方式。
帧模糊-Frame Blur
模拟运动模糊的最简单方法是获取前一帧的渲染目标(render tagert),并在该帧与当前帧的渲染目标之间进行插值。当可编程着色器首次出现时,就是这样做的。它非常简单易用,并且不需要对现有渲染管线进行任何更改。但是它并不真实,并且你无法对场景中的不同对象进行不同程度的模糊。
位置重建-Postion Reconstruction
帧模糊的进一步是位置重建。在这个方法中,我们正常的渲染场景。然后,我们对渲染目标中的每个像素采样深度缓冲区,并重建屏幕空间位置。使用先前帧的变换矩阵,然后我们计算该像素在先前帧的屏幕空间位置。之后我们可以计算屏幕空间的方向和距离,并模糊此像素。这个方法假定场景中的所有对象都是静态的。它认为帧缓冲区中像素的世界空间位置不会改变。因此,它非常适合模拟来自摄像机的运动,但如果您想要对场景中的动态对象模拟更细粒度的运动,那么它并不理想。
速度缓冲-Velocity Buffer
如果你确实需要处理动态对象,那么这就是适合你的解决方案。它也是三者中性能最昂贵的。这里我们需要对场景中的每个对象渲染两次,一次输出常规的场景渲染目标,第二次创建一个速度缓冲区(通常是R16G16的渲染目标)。你也可以通过绑定多个渲染目标(MRT)来规避第二次绘制调用。
当我们创建速度缓冲区时,我们从模型空间通过当前和先前的世界-视图-投影矩阵来变换我们渲染的每个对象。这样做我们也能够考虑到世界空间的变化。然后我们计算屏幕空间的变化并将此向量存储在速度缓冲区中。
实现-Implementation
要求-Requiremnets
我们决定实现位置重建方法。
- 帧模糊不是一种选择 - 这种方法太旧了,并不能提供足够的真实感。
- 我们游戏中的摄像机跟随着不断移动的玩家车辆,所以即使我们无法模拟世界空间变换,我们仍应该获得令人信服的效果。
- 我们不希望产生填充速度缓冲区的额外绘制调用成本。
- 我们不希望产生填充速度缓冲区的额外带宽开销。
- 我们不想消耗存储速度缓冲区所需的额外内存。
代码-Code
我们初始像往常一样渲染我们的场景。作为后处理步骤,我们在着色器中读取场景中每个像素的深度:
float depthBufferSample = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,uv).r;
然后我们将从深度重建屏幕空间位置:
float4 projPos; projPos.xy = uv.xy * 2.0 - 1.0; projPos.z = depthBufferSample; projPos.w = 1.0f;
在C#中,我们将转换矩阵传递到我们的着色器中。此矩阵将转换当前屏幕空间位置,如下所示:
- 相机空间-Camera Space
- 世界空间-World Space
- 先前帧的相机空间-Previous frames Camera Space
- 先前帧的屏幕空间-Previous frames Screen Space
这一切都是通过简单的乘法完成的:
float4 previous = mul(_CurrentToPreviousViewProjectionMatrix,projPos); previous / = previous.w;
要计算此转换矩阵,我们在C#中执行以下操作
private Matrix4x4 previousViewProjection; private void OnRenderImage(RenderTexture src, RenderTexture dest) { var viewProj = cam.projectionMatrix * cam.worldToCameraMatrix; var invViewProj = viewProj.inverse; var currentToPreviousViewProjectionMatrix = previousViewProjection * invViewProj; motionBlurMaterial.SetMatrix("_CurrentToPreviousViewProjectionMatrix", currentToPreviousViewProjectionMatrix); ... previousViewProjection = viewProj; }
我们现在可以计算两个屏幕空间向量之间的方向和距离。然后,我们使用距离作为缩放值,并沿着方向向量对渲染目标进行采样。
float2 blurVec = (previous.xy - projPos.xy) * 0.5f; float2 v = blurVec / NumberOfSamples; half4 colour; for(int i = 0; i < NumberOfSamples; ++i) { float2 uv = input.uv + (v * i); colour += tex2D(_MainTex, uv); } colour /= NumberOfSamples
控制运动模糊-Controlling the Motion Blur
一旦我们得到了屏幕的内容,我们很快就会发现太过于模糊。我们希望场景的大部分都模糊不清,但艺术家们希望车辆和驱动器是清晰的。为了实现这一目标,同时尽可能少的影响管线,我们决定使用alpha通道来屏蔽我们不想模糊的场景区域。然后,我们将此蒙版乘以模糊向量,以便有效地生成模糊向量[0,0]。
half4 colour = tex2D(_MainTex, input.uv); float mask = colour.a; for(int i = 1; i < NumberOfSamples; ++i) { float2 uv = input.uv + (v * mask * i); colour += tex2D(_MainTex, uv); }
除此之外,我们还发现远距离的物体不应该像前景中的物体那样模糊。为了实现这一点,我们简单地通过线性眼睛(视图空间)深度缩放模糊向量,从深度缓冲区(LinearEyeDepth)的计算是Unity的cginc头内的辅助函数。
float d = LinearEyeDepth(depthBufferSample); float depthScale = 1 - saturate(d / _DepthScale
结论-Conclusion
立即可用,Unity为你生成速度缓冲区来支持运动模糊,但是对于我们的要求,这是过度的。我们始终需要记住,我们是一个移动工作室,所以我们需要在每一步都考虑到性能。我们实现的方法有其权衡,我们必须添加基于距离的缩放以防止距离中的对象模糊太多。然而,由于我们的相机不断移动,它给我们带来了令人信服的效果。如果您有任何问题或反馈,请随时在Twitter上留言或在下面发表评论。