Distorting Object Shapes in Screen Space(屏幕空间扭曲)
2017-04-24 11:10
537 查看
,原作者链接
原作者效果
我的效果
渲染一张只包含非扭曲物体的原图像
渲染一张扭曲物体在最前面的扭曲图像
对扭曲图像进行扭曲处理
组合上面的两张图像完成扭曲特效
可以降低上面扭曲图像的分辨率,以提高性能。听起来怎么样?让我们开始吧!
这里有几点需要注意的地方:第一,我用一个缩放因子乘上屏幕的尺寸来调整扭曲图像大小。在低端设备中,降低分辨率可以提升性能(减少每帧处理的像素数量),图像越小,速度就越快,使用的内存也会越少。
第二,我设置了主相机的depthTextureMode 模式,这使我们能在着色器中拿到深度缓存(_CameraDepthTexture),它可以在我们进行两张图像合成时进行排序。
我们还需要在Update函数重更新一些变量:
这里没有什么特殊的。剩下的逻辑都会放到OnRenderImage函数里:
如果你为相机添加了这个脚本,然后运行,你会看到屏幕一片漆黑。
在渲染扭曲物体之前,我们需要复制原图像的颜色到distortingRT的RGB通道中。因为我们用的是低分辨率图像,这可能会使物体的边缘产生锯齿。看起来是这样:
我们还需要输出一个特殊的值到distortingRT的A通道,我们用它保存扭曲物体的深度信息,这个值在合成两张图像时可以进行先后顺序的判断。在此之前,我们需要给它一个默认值,它代表最远的深度值(对应远裁剪平面)。
如果你知道各个平台的差异的话,这很简单。某些平台(DX11/12和Metal)用1到0表示深度缓存,1代表最近的物体,0代表最远的物体。其他平台(OpenGL)相反,用的是0到1。我们需要输出最远的物体的深度值意味着,对于不同平台,这个值是不同的。
幸运的是,Unity有许多预处理器宏来帮助我们区分它们:
如果你不熟悉上面的图像处理代码,你可以在这找到全部源代码。
我们的着色器写好了,我们可以初步得到distortingRT了:
你可以看到我们没有重新分配RT的内存,而是使用RenderTexture.GetTemporary得到它。Unity文档是这么解释的:
如果你使用许多次“blits”操作,最好提前缓存1到2张来重复使用,而不是重新分配和释放它
这正是我们想要的,我们只需要在最后释放它就好。
如果你的平台不支持访问flaot类型RT,你可以使用默认的类型,但是它的精度较低,如果用这个精度的A值比较被扭曲的物体的深度,这可能产生错误的结果。小场景(小房间)可能没什么,但是大场景就会穿帮了。
在distortingRT 中,需要扭曲的物体会在任何物体之前。下面的图片中,机器人应该是在那些几何体之后的,但在distortingRT中,它会在前面。
这很重要,如果按正常的顺序的话,当我们的扭曲对象有一部分被遮挡时,我们会丢失这份颜色信息,被遮挡的边缘就不会有很好的扭曲效果了。所以我们需要将要扭曲的对象渲染到所有其他对象之前,之后手动比较深度值。好了,说的有点多了,我们该谈谈要被扭曲的物体的着色器了。
跟上面提到的一样,我们需要A通道作为深度缓存来使用,这样我们可以在混合(composite)着色器中通过_CameraDeapthTexture来比较前后顺序。虽然这不会很复杂,但还是需要注意一些平台的差异。
首先我们需要把数据从顶点着色器传到片段着色器,裁剪空间坐标的z和w值(假定你用MVP矩阵转换坐标)是我们想要的:
这个向量的z值可以表示这个点与相机的远近。不幸的是这个值远超(0,1)区间,所以我们要把它编码到(0,1)。我们让它除以w值进行透视除法,DirectX接口中,这个结果会在(0,1)之间,1代表远裁剪平面,0代表近裁剪平面,OpenGL接口是(-1,1),所以我们还要做一点数学运算将它映射到(0,1):
你修改着色器后,如果用alpha值(深度值)输出distortingRT的RGB通道,你应该看到下面的效果:
_MainTex是没有扭曲物体的图像。_DistortionRT 是刚才渲染的图像,扭曲物体部分RGB通道为其颜色,A通道为其深度,其余部分存储了场景的颜色和最远的深度值。_CameraDepthTexture 是_MainTex中像素的深度。因为我们之前指定了主相机的DepthMode,所以我们可以在着色器中访问它。最后用两个变量用来控制这个特效的程度,_DistortionOffset 控制扭曲特效偏移的频率,它是Time.time与一个常量值相乘的结果。这个值越高,特效的扭曲越快。_DistortionAmount 控制扭曲的幅度。改变这些值可以得到颤抖或者痉挛的效果。
了解了么?好的!我不会解释顶点着色器,因为它只是传递一些值:
所以让我们跳转到片段着色器。首先提取出_MainTex 和_CameraDepthTexture的值:
Unity对有的平台会翻转UV坐标,但可以确信的是_MainTex 不会翻转(我在GL、D3D11和Metal中测试过)。这虽然有点古怪,但应该不难理解,所以让我们进入稍微复杂的部分:提取_DistortionRT 的值
你可能对上面的代码有疑惑。对UV的横坐标进行上面的偏移,就会产生扭曲效果,偏移的方向是扭曲的反方向(将你左边的物体移动到你的位置,相对于这个物体右移)。我会直接跳过这个步骤,我以前的文章有详细的解释。
为了今天的目的,你现在还需要注意两件事:
这个UV运算会扭曲整个_DistortingRT ,所以用这个颜色返回的话会使整个屏幕都扭曲
A通道仍然包含深度
现在我们有了这些值,我们可以开始最后的步骤,对扭曲物体的排序。幸运的是,我们记录了这两张图的深度,比较它们就可以。_DistortingRT 的深度更近的话,我们输出_DistortingRT 刚才扭曲的颜色,否则输出原图像_MainTex的颜色。很简单对吧?
记住,不同平台以不同的方式处理深度。所以对于你的平台,你的比较有可能需要翻转,就像上面的那样。
混合(composite)着色器的片段函数的全部是下面这样:
我们最后要做的就是启用这个着色器,完全版本的OnRenderImage函数是这样的:
剩下要做的就是讨论一下一些遗留的细节和性能了。幸运的是,性能的讨论会很短:这个特效非常轻量级。用0.5的缩放因子(扭曲图像分辨率是全屏幕的一半),我的iPhone运行很流畅,当然你的扭曲图像越大,它的开销就越大,不过对于小屏幕,一半已经足够好了。
如果我的手机能运行这个…我想不用说,我的笔记本电脑也可以。我没有测试具体的数字,因为它是60帧的,我不想用我的周末做这些事。
其他可以提的是如何使这个特效更好的方法!正弦波的扭曲是非常低级的,如果你看过我的其他文章,你可以使用一些更有趣的模式。
此外,因为这所有都是在屏幕空间进行的,离相机很远的物体扭曲的幅度和近的是一样的,所以你可能需要用物体的深度值来缩放这个幅度值。
原作者效果
我的效果
整体思路
在我们进入实现的细节之前,先看一下这个整体思路:渲染一张只包含非扭曲物体的原图像
渲染一张扭曲物体在最前面的扭曲图像
对扭曲图像进行扭曲处理
组合上面的两张图像完成扭曲特效
可以降低上面扭曲图像的分辨率,以提高性能。听起来怎么样?让我们开始吧!
初始化设置
我们将所有要设置的东西放在一个附在主相机的C#脚本中。我用两个相机实现这个特效,物体的划分用遮挡剔除来完成,主相机渲染非扭曲物体,另一个相机渲染扭曲物体。private Camera cam; private Camera maskCam; public Material compositeMat; public Material stripAlphaMat; public float speed = 0.5f; public float scaleFactor = 1.0f; public float magnitude = 0.1f; private int scaledWidth; private int scaledHeight; void Start () { cam = GetComponent<Camera>(); scaledWidth = (int)(Screen.width * scaleFactor); scaledHeight = (int)(Screen.height * scaleFactor); cam.cullingMask = ~(1 << LayerMask.NameToLayer("Distortion")); cam.depthTextureMode = DepthTextureMode.Depth; maskCam = new GameObject("Distort Mask Cam").AddComponent<Camera>(); maskCam.enabled = false; maskCam.clearFlags = CameraClearFlags.Nothing; }
这里有几点需要注意的地方:第一,我用一个缩放因子乘上屏幕的尺寸来调整扭曲图像大小。在低端设备中,降低分辨率可以提升性能(减少每帧处理的像素数量),图像越小,速度就越快,使用的内存也会越少。
第二,我设置了主相机的depthTextureMode 模式,这使我们能在着色器中拿到深度缓存(_CameraDepthTexture),它可以在我们进行两张图像合成时进行排序。
我们还需要在Update函数重更新一些变量:
void Update () { scaleFactor = Mathf.Clamp(scaleFactor, 0.01f, 1.0f); scaledWidth = (int)(Screen.width * scaleFactor); scaledHeight = (int)(Screen.height * scaleFactor); magnitude = Mathf.Max(0.0f, magnitude); Shader.SetGlobalFloat("_DistortionOffset", -Time.time * speed); Shader.SetGlobalFloat("_DistortionAmount", magnitude/100.0f); }
这里没有什么特殊的。剩下的逻辑都会放到OnRenderImage函数里:
private void OnRenderImage(RenderTexture src, RenderTexture dst) { //cool stuff goes here :) }
如果你为相机添加了这个脚本,然后运行,你会看到屏幕一片漆黑。
渲染扭曲图像
和上面说的一样,我们要做的第一件事是用某些颜色(和深度)填充我们的扭曲图像。在OnRenderImage函数中,我们能取到这一帧屏幕的原图像(函数的src参数,没有扭曲物体的图像),我们可以按我们的需求降低这张图像的分辨率。(我用“distortingRT”代表扭曲图像)在渲染扭曲物体之前,我们需要复制原图像的颜色到distortingRT的RGB通道中。因为我们用的是低分辨率图像,这可能会使物体的边缘产生锯齿。看起来是这样:
我们还需要输出一个特殊的值到distortingRT的A通道,我们用它保存扭曲物体的深度信息,这个值在合成两张图像时可以进行先后顺序的判断。在此之前,我们需要给它一个默认值,它代表最远的深度值(对应远裁剪平面)。
如果你知道各个平台的差异的话,这很简单。某些平台(DX11/12和Metal)用1到0表示深度缓存,1代表最近的物体,0代表最远的物体。其他平台(OpenGL)相反,用的是0到1。我们需要输出最远的物体的深度值意味着,对于不同平台,这个值是不同的。
幸运的是,Unity有许多预处理器宏来帮助我们区分它们:
fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); #if UNITY_REVERSED_Z col.a = 0.0; #else col.a = 1.0; #endif return col; }
如果你不熟悉上面的图像处理代码,你可以在这找到全部源代码。
我们的着色器写好了,我们可以初步得到distortingRT了:
private void OnRenderImage(RenderTexture src, RenderTexture dst) { RenderTexture distortingRT = RenderTexture.GetTemporary(scaledWidth, scaledHeight, 24); Graphics.Blit(src, distortingRT, stripAlphaMat); }
你可以看到我们没有重新分配RT的内存,而是使用RenderTexture.GetTemporary得到它。Unity文档是这么解释的:
如果你使用许多次“blits”操作,最好提前缓存1到2张来重复使用,而不是重新分配和释放它
这正是我们想要的,我们只需要在最后释放它就好。
渲染扭曲物体
我们接下来要将需要扭曲的物体渲染到distortingRT中去,除了以防其他脚本打乱渲染效果而每帧都重置相机的参数外,这段代码没什么特别的。private void OnRenderImage(RenderTexture src, RenderTexture dst) { RenderTexture distortingRT = RenderTexture.GetTemporary(scaledWidth, scaledHeight, 24, RenderTextureFormat.ARGBFloat); Graphics.Blit(src, distortingRT, stripAlphaMat); maskCam.CopyFrom(cam); maskCam.clearFlags = CameraClearFlags.Depth; maskCam.gameObject.transform.position = transform.position; maskCam.gameObject.transform.rotation = transform.rotation; maskCam.cullingMask = 1 << LayerMask.NameToLayer("Distortion"); maskCam.SetTargetBuffers(distortingRT.colorBuffer, distortingRT.depthBuffer); maskCam.Render(); }
如果你的平台不支持访问flaot类型RT,你可以使用默认的类型,但是它的精度较低,如果用这个精度的A值比较被扭曲的物体的深度,这可能产生错误的结果。小场景(小房间)可能没什么,但是大场景就会穿帮了。
在distortingRT 中,需要扭曲的物体会在任何物体之前。下面的图片中,机器人应该是在那些几何体之后的,但在distortingRT中,它会在前面。
这很重要,如果按正常的顺序的话,当我们的扭曲对象有一部分被遮挡时,我们会丢失这份颜色信息,被遮挡的边缘就不会有很好的扭曲效果了。所以我们需要将要扭曲的对象渲染到所有其他对象之前,之后手动比较深度值。好了,说的有点多了,我们该谈谈要被扭曲的物体的着色器了。
扭曲物体的着色器
扭曲物体的着色器你可以任意使用,只需要修改一下A通道的输出值就行了。对于不透明物体,这没什么问题,因为它们不使用A通道。但是半透明需要A通道做透明混合,所以需要在第二个Pass中写入alpha值。跟上面提到的一样,我们需要A通道作为深度缓存来使用,这样我们可以在混合(composite)着色器中通过_CameraDeapthTexture来比较前后顺序。虽然这不会很复杂,但还是需要注意一些平台的差异。
首先我们需要把数据从顶点着色器传到片段着色器,裁剪空间坐标的z和w值(假定你用MVP矩阵转换坐标)是我们想要的:
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
这个向量的z值可以表示这个点与相机的远近。不幸的是这个值远超(0,1)区间,所以我们要把它编码到(0,1)。我们让它除以w值进行透视除法,DirectX接口中,这个结果会在(0,1)之间,1代表远裁剪平面,0代表近裁剪平面,OpenGL接口是(-1,1),所以我们还要做一点数学运算将它映射到(0,1):
float4 frag (v2f i) : SV_Target { //other shading logic fills RGB channels col.a = (i.screen.z / i.screen.w); //using UNITY_REVERSED_Z becuase SHADER_TARGET_GLSL //doesn't seem to work on my machine #if !defined(UNITY_REVERSED_Z) col.a = (col.a + 1.0) * 0.5; #endif return col; }
你修改着色器后,如果用alpha值(深度值)输出distortingRT的RGB通道,你应该看到下面的效果:
混合(Composite)着色器
现在所需要做的就是将这些组合在一起。混合(composite)着色器是到目前为止最复杂的着色器,所以我会展示更多的代码。让我们先看一下需要传递到着色器中的数据:sampler2D _MainTex; float4 _MainTex_ST; sampler2D _DistortionRT; sampler2D _CameraDepthTexture; float _DistortionOffset; float _DistortionAmount;
_MainTex是没有扭曲物体的图像。_DistortionRT 是刚才渲染的图像,扭曲物体部分RGB通道为其颜色,A通道为其深度,其余部分存储了场景的颜色和最远的深度值。_CameraDepthTexture 是_MainTex中像素的深度。因为我们之前指定了主相机的DepthMode,所以我们可以在着色器中访问它。最后用两个变量用来控制这个特效的程度,_DistortionOffset 控制扭曲特效偏移的频率,它是Time.time与一个常量值相乘的结果。这个值越高,特效的扭曲越快。_DistortionAmount 控制扭曲的幅度。改变这些值可以得到颤抖或者痉挛的效果。
了解了么?好的!我不会解释顶点着色器,因为它只是传递一些值:
v2f vert(appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = TRANSFORM_TEX(v.uv,_MainTex); return o; }
所以让我们跳转到片段着色器。首先提取出_MainTex 和_CameraDepthTexture的值:
fixed4 frag(v2f i) : SV_Target { fixed4 screen = tex2D(_MainTex, float2(i.uv.x, i.uv.y)); float2 distortUVs = i.uv; #if defined(UNITY_UV_STARTS_AT_TOP) && !defined(SHADER_API_MOBILE) distortUVs.y = 1.0 - distortUVs.y; #endif float d = tex2D(_CameraDepthTexture, distortUVs).r;//注意这里的深度用的是扭曲点的深度,非原像素深度
Unity对有的平台会翻转UV坐标,但可以确信的是_MainTex 不会翻转(我在GL、D3D11和Metal中测试过)。这虽然有点古怪,但应该不难理解,所以让我们进入稍微复杂的部分:提取_DistortionRT 的值
float4 distort = tex2D(_DistortionRT, fixed2(distortUVs.x + sin((distortUVs.y + _DistortionOffset) * 100)*_DistortionAmount, distortUVs.y));
你可能对上面的代码有疑惑。对UV的横坐标进行上面的偏移,就会产生扭曲效果,偏移的方向是扭曲的反方向(将你左边的物体移动到你的位置,相对于这个物体右移)。我会直接跳过这个步骤,我以前的文章有详细的解释。
为了今天的目的,你现在还需要注意两件事:
这个UV运算会扭曲整个_DistortingRT ,所以用这个颜色返回的话会使整个屏幕都扭曲
A通道仍然包含深度
现在我们有了这些值,我们可以开始最后的步骤,对扭曲物体的排序。幸运的是,我们记录了这两张图的深度,比较它们就可以。_DistortingRT 的深度更近的话,我们输出_DistortingRT 刚才扭曲的颜色,否则输出原图像_MainTex的颜色。很简单对吧?
#if UNITY_REVERSED_Z return lerp(screen, distort, distort.a > d); #else return lerp(screen, distort, distort.a < d); #endif
记住,不同平台以不同的方式处理深度。所以对于你的平台,你的比较有可能需要翻转,就像上面的那样。
混合(composite)着色器的片段函数的全部是下面这样:
fixed4 frag(v2f i) : SV_Target
{
fixed4 screen = tex2D(_MainTex, float2(i.uv.x, i.uv.y));
float2 distortUVs = i.uv;
#if defined(UNITY_UV_STARTS_AT_TOP) && !defined(SHADER_API_MOBILE)
distortUVs.y = 1.0 - distortUVs.y;
#endif
float4 distort = tex2D(_DistortionRT, fixed2(distortUVs.x + sin((distortUVs.y + _DistortionOffset) * 100)*_DistortionAmount, distortUVs.y));
float d = tex2D(_CameraDepthTexture, distortUVs).r;
#if UNITY_REVERSED_Z return lerp(screen, distort, distort.a > d); #else return lerp(screen, distort, distort.a < d); #endif
}
我们最后要做的就是启用这个着色器,完全版本的OnRenderImage函数是这样的:
private void OnRenderImage(RenderTexture src, RenderTexture dst) { RenderTexture distortingRT = RenderTexture.GetTemporary(scaledWidth, scaledHeight, 24, RenderTextureFormat.ARGBFloat); Graphics.Blit(src, distortingRT, stripAlphaMat); maskCam.CopyFrom(cam); maskCam.gameObject.transform.position = transform.position; maskCam.gameObject.transform.rotation = transform.rotation; //draw the distorting objects into the buffer maskCam.clearFlags = CameraClearFlags.Depth; maskCam.cullingMask = 1 << LayerMask.NameToLayer("Distortion"); maskCam.SetTargetBuffers(distortingRT.colorBuffer, distortingRT.depthBuffer); maskCam.Render(); //Composite pass compositeMat.SetTexture("_DistortionRT", distortingRT); Graphics.Blit(src, dst, compositeMat); RenderTexture.ReleaseTemporary(distortingRT); }
性能和其他考虑
现在我们的特效成功了。如果你没有跟上某一部分,或者你很懒,所有的代码都在GitHub上。剩下要做的就是讨论一下一些遗留的细节和性能了。幸运的是,性能的讨论会很短:这个特效非常轻量级。用0.5的缩放因子(扭曲图像分辨率是全屏幕的一半),我的iPhone运行很流畅,当然你的扭曲图像越大,它的开销就越大,不过对于小屏幕,一半已经足够好了。
如果我的手机能运行这个…我想不用说,我的笔记本电脑也可以。我没有测试具体的数字,因为它是60帧的,我不想用我的周末做这些事。
其他可以提的是如何使这个特效更好的方法!正弦波的扭曲是非常低级的,如果你看过我的其他文章,你可以使用一些更有趣的模式。
此外,因为这所有都是在屏幕空间进行的,离相机很远的物体扭曲的幅度和近的是一样的,所以你可能需要用物体的深度值来缩放这个幅度值。
相关文章推荐
- 屏幕空间的近似全局光照明(Approximative Global Illumination in Screen Space)
- Unity3d 屏幕空间人体皮肤知觉渲染&次表面散射Screen-Space Perceptual Rendering & Subsurface Scattering of Human Skin
- 在Unity中实现屏幕空间反射Screen Space Reflection(1)
- Directx11教程四十一之SSAO(ScreenSpaceAmcientOccusion,屏幕空间环境遮挡)
- 高级屏幕空间反射: Screen Space Reflection (SSR)
- 在Unity中实现屏幕空间反射Screen Space Reflection(3)
- 在Unity中实现屏幕空间反射Screen Space Reflection(4)
- 在Unity中实现屏幕空间反射Screen Space Reflection(2)
- 高级屏幕空间反射: Screen Space Reflection (SSSR)
- [置顶] Unity3d 屏幕空间人体皮肤知觉渲染&次表面散射Screen-Space Perceptual Rendering & Subsurface Scattering of Human Skin
- android获得控件在屏幕中的绝对坐标 getLocationInWindow 和 getLocationOnScreen
- Screen Space Reflections in Unity 5
- FUSE(Filesystem in userspace)(用户空间文件系统),user-space框架简单介绍
- android获得控件在屏幕中的绝对坐标 getLocationInWindow 和 getLocationOnScreen
- Cg Programming/Unity/Shading in World Space世界空间中的着色器
- android获得控件在屏幕中的绝对坐标 getLocationInWindow 和 getLocationOnScreen
- Could not allocate space for object in database because the filegroup is full
- MDK在链接时提示空间不够(No space in execution regions with .ANY selector... )的解决方案总结
- android获得控件在屏幕中的绝对坐标 getLocationInWindow 和 getLocationOnScreen
- 使用getLocationInWindow或getLocationOnScreen获得View在屏幕中的坐标