# 前言
最近快要离职了,自己也打算之后做一个 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。
现在的效果 (应该能看出不同吧?):
# 扭曲
OK, 接下来就是正式操作的时候了。
首先,我们可以来看一个矩阵:
学过计算机图形学的看见就知道,这就是个二维旋转矩阵。而我们这儿也正是要用到它来实现旋转扭曲。
在这儿,首先我们需要明白,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 进行旋转之后挪回去。
就是这样。
...
....
.....
...... 为什么我觉得效果完全不是想象中的好吧?
为啥看起来这么死板?说好的像是扭曲空间的效果呢?
嘛... 果然只修改像素完全没法玩,先放这儿吧,下次得找个新方法才行。
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
}
}
}