做一个简单的睁眼苏醒效果,也可用于眨眼、闭眼、昏睡等等:
本篇文章中介绍的思路适用于Built-in渲染管线,如果您需要在URP中实现,可参考我的这篇文章,本篇文章末尾也有URP对应的仓库地址。
首先编写一个AwakeScreenEffect.cs脚本:
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class AwakeScreenEffect : MonoBehaviour
{
public Shader shader;
[SerializeField]
Material material;
Material Material
{
get
{
if (material == null)
{
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
}
return material;
}
}
void OnDisable()
{
if (material)
{
DestroyImmediate(material);
}
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
// TODO 处理...
}
}
将脚本挂在相机上,接下来编写相应的shader。目标效果中,从闭眼到睁眼的过程用一个进度0~1表示,当进度从0到1时,眼睛逐渐睁开,视野从模糊逐渐变为清晰。
创建AwakeScreenEffect.shader,_Progress表示当前苏醒进度:
Shader "Custom/Awake Screen Effect"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Progress ("Progress", Range(0, 1)) = 1
}
SubShader
{
ZTest Always ZWrite Off Cull Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
half2 uv : TEXCOORD0;
};
struct v2f
{
half2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float _Progress;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half2 uv = i.uv;
fixed4 col = tex2D(_MainTex, uv);
// TODO ...
return col;
}
ENDCG
}
}
Fallback Off
}
先写上下眼皮,它们的边界值分别是屏幕中线(0.5)加或减去当前进度乘以0.5,得出边界值后通过step函数对uv.v进行裁剪,大于上眼皮边界、小于下眼皮边界时裁剪值为0,否则为1,最后将它与颜色相乘得出效果:
fixed4 frag (v2f i) : SV_Target
{
half2 uv = i.uv;
fixed4 col = tex2D(_MainTex, uv);
// 上眼皮与下眼皮边界
float upBorder = .5 + _Progress * .5;
float downBorder = .5 - _Progress * .5;
// 可视区域
float visibleV = (1 - step(upBorder, uv.y)) * (step(downBorder, uv.y));
col *= visibleV;
return col;
}
AwakeScreenEffect.cs中加入可调节的进度变量,OnRenderImage方法中应用:
...
public class AwakeScreenEffect : MonoBehaviour
{
[Range(0f, 1f)][Tooltip("苏醒进度")]
public float progress;
...
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
Material.SetFloat("_Progress", progress);
Graphics.Blit(src, dest, material);
}
}
可以看到初步效果:
除非是能人异士,不然大部分人的眼皮都不是这样一条直线,shader中定义一个眼皮弧形的高度值_ArchHeight:
Properties
{
...
_ArchHeight ("Arch Height", Range (0, .5)) = .2
}
用二次函数做出弧度:
float _ArchHeight;
fixed4 frag (v2f i) : SV_Target
{
...
// 上眼皮与下眼皮边界
float upBorder = .5 + _Progress * (.5 + _ArchHeight);
float downBorder = .5 - _Progress * (.5 + _ArchHeight);
upBorder -= _ArchHeight * pow(uv.x - .5, 2);
downBorder += _ArchHeight * pow(uv.x - .5, 2);
...
}
上下边界由原来的* .5
改为* (.5 + _ArchHeight)
,用来调整上下边界随_Progress的变化范围,避免_Progress为1时仍有黑边的情况。
再加入模糊,模糊效果直接使用了《Unity Shader 入门精要》中的高斯模糊,新建一个GaussianBlur.shader,拷贝代码,确认一下shader和Pass的命名无误:
Shader "Custom/Gaussian Blur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1
}
SubShader
{
...
Pass
{
NAME "GAUSSIAN_BLUR_VERTICAL"
...
}
Pass
{
NAME "GAUSSIAN_BLUR_HORIZONTAL"
...
}
}
...
}
在AwakeScreenEffect.shader中原有Pass之后使用高斯模糊的两个Pass:
SubShader
{
...
Pass
{
...
}
...
UsePass "Custom/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"
UsePass "Custom/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
}
AwakeScreenEffect.cs加入模糊需要用到的参数:
[Range(0, 4)][Tooltip("模糊迭代次数")]
public int blurIterations = 3;
[Range(.2f, 3f)][Tooltip("每次模糊迭代时的模糊大小扩散")]
public float blurSpread = .6f;
修改OnRenderImage方法,基本和书中的差不多,不同的是没有使用降采样。随着progress从0变为1,blurSize也逐渐变为0。
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
Material.SetFloat("_Progress", progress);
if (progress < 1)
{
// 由于降采样会影响模糊到清晰的连贯性,这里没有使用
int rtW = src.width;
int rtH = src.height;
var buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0, Material, 0); // 眼皮Pass
// 模糊
float blurSize;
for (int i = 0; i < blurIterations; i++)
{
// 将progress(0~1)映射到blurSize(blurSize~0)
blurSize = 1f + i * blurSpread;
blurSize = blurSize - blurSize * progress;
Material.SetFloat("_BlurSize", blurSize);
// 竖直方向的Pass
var buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, Material, 1);
RenderTexture.ReleaseTemporary(buffer0);
// 竖直方向的Pass
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, Material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
// 完全苏醒则无需处理,直接blit
Graphics.Blit(src, dest);
}
}
画面由暗转亮比较简单,AwakeScreenEffect.shader让颜色乘以_Progress即可:
fixed4 frag (v2f i) : SV_Target
{
...
col *= _Progress;
return col;
}
最后用Animator录制一个动画就完成了。
Demo项目地址:
Procedural-Map-Demo(Built-in渲染管线)
pamisu-kit-unity(URP自定义后处理)