迷幻角色背景
大家好,我是阿赵。
之前介绍过了经典的Shader写法,物体顶点坐标在顶点程序转换到裁剪空间,然后在片段程序里面通过模型的UV进行贴图采样,然后把颜色显示在模型上面。
之前也介绍过经典的顶点程序应用,树木的生长。通过特定的信息,控制顶点的偏移,模拟出树干树枝生长的效果。
这次带来一个我觉得比较经典的UV坐标控制例子。
一、例子说明
在做这个例子之前,先来考虑一个问题。我们采样模型的贴图,是不是必须使用模型的UV信息?
答案肯定是否的。
我之前在很多例子里面已经用过MatCap效果,采样MatCap就并不是用模型的UV坐标,而是把物体的法线方向转到观察空间,然后再转换到[0,1]的范围,作为采样MatCap贴图的UV信息,可以看代码回顾一下:
float2 GetMatCapUV(float3 objNormal)
{
float3 normalWorld = mul(unity_ObjectToWorld, objNormal);
float3 normalView = mul(UNITY_MATRIX_IT_MV, normalWorld);
return normalView.xy*0.5+0.5;
}
在这次这个例子里面,我做了一个使用顶点坐标转屏幕空间坐标的操作,使用这个屏幕坐标作为UV来采样贴图。这样得到的效果,模型上的贴图会一直正面朝着屏幕,不会因为模型的旋转或者摄像机的旋转而导致贴图的方向发生变化。模型的顶点范围,只会影响到贴图显示的范围大小。
为了让这个例子看起来更有意思,所以我使用了2个视频作为贴图,所以我先会说一下在Unity里面怎样使用AVProVideo插件播放视频。
二、详细做法说明
1、AVProVideo播放视频
1.导入AVProVideo
先购买插件,然后导入项目,会看到有一个AVProVideo文件夹,里面会有Demo,想深入了解的话可以去看看各种demo的应用。我这个例子用的不是很复杂,只是单纯的用来播放视频而已,所以那些360播放、VR播放的功能,就不去管了。
2.创建播放器
导入插件之后,在创建物体对象的地方就可以创建MediaPlayer,在场景里面创建MediaPlayer。
3.指定播放的视频
选择MediaPlayer,可以看到选项。资源路径里面可以指定播放视频的地址。然后可以设置自动打开、自动播放、循环等。
4.渲染到指定的材质球
添加一个ApplyToMaterial的组件,可以把指定的MediaPlayer渲染的视频,以RenderTexture的形式传入指定的材质球。
5.为例子准备的2个视频
(1)背景视频
这个背景是我自己用AE做出来的一个循环背景,如果有人有兴趣,可以提出来,我下次再告诉大家怎样做。
(2)角色身上的视频
这个是AVProVideo插件的demo自带的视频,本来是一个可以贴到球体上的循环视频,我觉得效果不错,就直接拿来做例子了。
2、制作背景材质
背景其实就是是一个面片,但由于没有拿模型的UV坐标作为采样,而是拿了顶点坐标转屏幕坐标作为UV来采样,所以不管怎样旋转面片,显示的画面都会是正对着我们的
关键的计算步骤是:
1.顶点程序
o.screen_pos = ComputeScreenPos(o.vertex);
其中ComputeScreenPos函数是UnityCG.cginc内置的,通过这个方法获取到的屏幕坐标的取值范围已经是[0,1],比自己计算方便。
inline float4 ComputeScreenPos (float4 pos)
{
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
o.zw = pos.zw;
return o;
}
2.片段程序
//用透视除法,将裁剪空间坐标转换到NDC空间坐标
float2 screenUV = i.screen_pos.xy / (i.screen_pos.w + 0.000001);
float aspect = _ScreenParams.x / _ScreenParams.y;
float aspectx = aspect * _scale.x;
screenUV.x *= aspectx;
screenUV.x += (1 - aspectx) / 2;
//判断屏幕UV是否左上角是0,是否需要上下翻转V坐标。
//由于例子用的是视频插件生成的RenderTexture,没有Repeat,所以不能直接UV坐标乘以-1,改为用1-V坐标
if(_ProjectionParams.x < 0)
{
screenUV.y = 1 - screenUV.y;
}
//用屏幕UV采样贴图,得到颜色1
half4 col = tex2D(_MainTex, screenUV);
这里的重点是用透视除法求NDC空间的坐标。然后用_ScreenParams.x / _ScreenParams.y;求出屏幕的长宽比例,进行偏移,让画面可以在屏幕中间居中。
不过如果单纯是这样计算出UV然后进行采样,会发现画面是上下颠倒的,所以还要根据_ProjectionParams.x判断屏幕UV是否左上角是0,是否需要上下翻转V坐标。
最后就可以把计算出来的UV坐标来采样贴图了。
3、角色材质制作
角色的材质是在背景材质上做了加工。
这里分别用2套UV对角色模型进行了采样,一套是正常的模型UV、另外一套是顶点坐标转屏幕坐标(和背景的计算方式一样)
两套UV都是采样传进去的视频作为贴图,具体的效果如下:
接下来要做的事情就非常简单了,我很喜欢用一张噪声图,把它自身的UV平铺数值改成0,0,然后用一个_Time.y来滚动UV坐标,就能采样噪声图的单个像素,做出黑白灰闪动的效果:
float noiseUV = i.uv*_noiseMap_ST.xy + _noiseMap_ST.zw;
noiseUV.x += _Time.y*_speed;
float4 noiseCol = tex2D(_noiseMap, noiseUV);
float noiseVal = step(0.5, noiseCol.r);
最后,对黑白灰的效果做step,让它只会出现黑白,然后用来混合刚才两套UV的采样结果,就做出了2种效果之间的闪烁变化了。
float4 finalCol = col * noiseVal + col2 * (1 - noiseVal);
三、完整Shader
Shader "azhao/CenterImgRole"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_scale("Scale",Vector) = (1,1,1,1)
_noiseMap("Noise",2D) = "black"{}
_speed("Speed",float) = 1
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float4 screen_pos:TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _scale;
float _speed;
sampler2D _noiseMap;
float4 _noiseMap_ST;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//获取顶点对应的屏幕空间坐标
o.screen_pos = ComputeScreenPos(o.vertex);
return o;
}
half4 frag(v2f i) : SV_Target
{
//用透视除法,将裁剪空间坐标转换到NDC空间坐标
float2 screenUV = i.screen_pos.xy / (i.screen_pos.w + 0.000001);
float aspect = _ScreenParams.x / _ScreenParams.y;
float aspectx = aspect * _scale.x;
screenUV.x *= aspectx;
screenUV.x += (1 - aspectx) / 2;
//判断屏幕UV是否左上角是0,是否需要上下翻转V坐标。
//由于例子用的是视频插件生成的RenderTexture,没有Repeat,所以不能直接UV坐标乘以-1,改为用1-V坐标
if(_ProjectionParams.x < 0)
{
screenUV.y = 1 - screenUV.y;
}
//用屏幕UV采样贴图,得到颜色1
half4 col = tex2D(_MainTex, screenUV);
//用正常模型坐标采样贴图,得到颜色2
half4 col2 = tex2D(_MainTex, i.uv);
//用模型坐标采样一张噪声贴图,用于混合颜色1和颜色2
float noiseUV = i.uv*_noiseMap_ST.xy + _noiseMap_ST.zw;
noiseUV.x += _Time.y*_speed;
float4 noiseCol = tex2D(_noiseMap, noiseUV);
float noiseVal = step(0.5, noiseCol.r);
float4 finalCol = col * noiseVal + col2 * (1 - noiseVal);
return finalCol;
}
ENDCG
}
}
}