# 前言

最近快要离职了,自己也打算之后做一个 Demo,主题是 “魔法” 与 “科技”。
既然都有魔法了,不想一点效果是不行的。于是想到曾经在电视里看到的那种,类似于从玻璃瓶子去看世界的 “空间扭曲” 效果。

我首先想到的是:抓取当前显示内容,对像素进行旋转,使其造成扭曲的效果。

# 操作 “像素” 扭曲

# 屏幕图像读取

因为基本上可以算是纯 Shader 实现,所以就不用管其他脚本之类的了。
在 Unity 的 Shader 中,抓取当前屏幕最简单的方式是使用 GrabPass。

所以,新建一个 Shader,首先在默认 Pass 之前,加上一个 GrabPass 吧:

	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100
		GrabPass{}
		Pass
		//暂时省略

然后在 Properties 中,添加上扭曲度的设置,这个用来控制扭曲效果(当然现在是因为测试用才加上的,方便手动测试效果):

	Properties
	{
		_Twist("Twist",FLOAT) = 1
	}

然后,把那些操作 MainTex 之类的自动生成代码都删了(因为我们不需要贴图,是直接使用的当前屏幕图片),再加上抓取的屏幕图片与扭曲参数的声明:

		uniform float _Twist;
		uniform sampler2D _GrabTexture;

最后,将对 MainTex 的读取换成对 GrabTexture 的,那么将会看到这样的效果:

直接读取图片

现在就是直接将当前相机 “看到” 的图片直接显示出来而已,接下来,我们需要对其进行一些 “加工”。因为默认情况下,截取到的 GrabPass 包括了整个屏幕的图像,而我们的扭曲,只需要 “当前”,就如同上图那块 Plane 所遮挡住的图像。所以,再次就需要处理一下 UV 了。

在 vert 代码段中,加入对 UV 的计算:

			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				float4 screenPos = ComputeGrabScreenPos(o.vertex);
				o.uv = screenPos.xy / screenPos.w;
				return o;
			}

这里用到的是 Unity 提供的一个内置函数 “ComputeGrabScreenPos”,专门用以计算 GrabPass 提取图片在模型本身位置,然后 X、Y 除以 W 使齐次坐标转为二维的 UV。

现在的效果 (应该能看出不同吧?):

处理UV后

# 扭曲

OK, 接下来就是正式操作的时候了。

首先,我们可以来看一个矩阵:

{cosθsinθsinθcosθ}\begin{Bmatrix} \cos\theta&-\sin\theta\\ \sin\theta&\cos\theta\\ \end{Bmatrix}

学过计算机图形学的看见就知道,这就是个二维旋转矩阵。而我们这儿也正是要用到它来实现旋转扭曲。
在这儿,首先我们需要明白,UV 的坐标。它是以左下角为(0,0),右上角为(1,1)。上述旋转矩阵则以(0,0)点为旋转中心。所以默认情况下,扭曲会以左下角为准。为了让这个中心点挪到中点,那么旋转的时候就得偏移一下。
如下:

fixed2 uv = i.uv;
fixed2 moveUV =  fixed2(uv.x-0.5,uv.y-0.5);

... 再说怎么计算上述矩阵吧。
为了计算这个矩阵,我们需要先计算出正弦和余弦。
为了计算正弦和余弦,我们需要先计算出旋转角度 (弧度)。
因为 CG 语言本身提供了 sin (x)、sincos (float x, out s, out c) 方法来直接计算正弦余弦,所以,我们只需要计算出旋转弧度即可获取到这两者的值。

				//计算出度转弧度
				float deg2rad = 3.14 / 180;
				//扭曲量与距离成反比
				float rad =_Twist* deg2rad / length(moveUV);
				float s, c;
				sincos(rad,s,c);
				float2x2 mat = float2x2(c,-s,s,c);
				moveUV = mul(mat, moveUV) +0.5;

下面来解释下吧。
deg2rad 大家都明白,是度数转化为弧度所需的一个值 —— 实际上只是个常量,这儿为了便于查看儿写下,其实可以直接写死。
后面_Twist* deg2rad 就是把输入的扭曲度数转化为弧度,再除以 UV 的长度,则是为了令扭曲量随着距离的增加而变小 —— 漩涡不都是这样的嘛?

另外,_Twist* deg2rad 这一段计算全都可以扔外部去,然后传进来使用的。

后面,构建旋转矩阵,将 UV 进行旋转之后挪回去。

就是这样。

效果

GIF效果(加上其它景象太大了,上个天空的吧)

...

....

.....

...... 为什么我觉得效果完全不是想象中的好吧?

为啥看起来这么死板?说好的像是扭曲空间的效果呢?

嘛... 果然只修改像素完全没法玩,先放这儿吧,下次得找个新方法才行。

PS: 网上也找到过类似的实现方法,不过说明并不详细。

# 源码

差点忘了...

Shader "Unlit/Twist"
{
	Properties
	{
		_Twist("Twist",FLOAT) = 1
	}
		SubShader
	{
		Tags { "RenderType" = "Opaque" }
		LOD 100
		GrabPass{}
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			uniform float _Twist;
			uniform sampler2D _GrabTexture;

			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				float4 screenPos = ComputeGrabScreenPos(o.vertex);
				o.uv = screenPos.xy / screenPos.w;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed2 uv = i.uv;
				fixed2 moveUV =  fixed2(uv.x-0.5,uv.y-0.5);
				if (_Twist > 360)
					_Twist = _Twist - 360;
				//计算出度转弧度
				float deg2rad = 3.14 / 180;
				//扭曲量与距离成反比
				float rad =_Twist* deg2rad / length(moveUV);
				float s, c;
				sincos(rad,s,c);
				float2x2 mat = float2x2(c,-s,s,c);
				moveUV = mul(mat, moveUV) +0.5;

				// sample the texture
				fixed4 col = tex2D(_GrabTexture, moveUV);
				return col;
			}
			ENDCG
		}
	}
}