您的位置:首页 > 移动开发 > Objective-C

Distorting Object Shapes in Screen Space(屏幕空间扭曲)

2017-04-24 11:10 537 查看
原作者链接



原作者效果


我的效果

整体思路

在我们进入实现的细节之前,先看一下这个整体思路:

渲染一张只包含非扭曲物体的原图像

渲染一张扭曲物体在最前面的扭曲图像

对扭曲图像进行扭曲处理

组合上面的两张图像完成扭曲特效

可以降低上面扭曲图像的分辨率,以提高性能。听起来怎么样?让我们开始吧!

初始化设置

我们将所有要设置的东西放在一个附在主相机的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帧的,我不想用我的周末做这些事。

其他可以提的是如何使这个特效更好的方法!正弦波的扭曲是非常低级的,如果你看过我的其他文章,你可以使用一些更有趣的模式。

此外,因为这所有都是在屏幕空间进行的,离相机很远的物体扭曲的幅度和近的是一样的,所以你可能需要用物体的深度值来缩放这个幅度值。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐