[Unity]第一人称睁眼苏醒效果
-
做一个简单的睁眼苏醒效果,也可用于眨眼、闭眼、昏睡等等:
本篇文章中介绍的思路适用于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 入门精要》12.4节中的高斯模糊,新建一个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自定义后处理)