一直以来我都非常崇拜那些能写出特别炫酷的特效的大神,每当看到游戏中一些炫酷的特效时,我都在想这些是怎么实现的,希望自己有一天也能亲自写出这些特效,一直都想去学习图形学相关的知识,说来惭愧由于自己的拖延症加上懒,一直拖在了最近才开始学习一些shader相关的知识,前段时间刚看完《unity shader入门精要》这本书,在这当中也学到了许多shader的一些知识,当我知道这本书的作者是一个女生时我非常惊讶,没想到是一个程序媛小姐姐,还专门去百度了一下并关注了她的博客,并被她的博文给深深吸引住了,她真的太厉害了。再对比一下自己,瞬间感到无地自容。
自此开始了我的shader学习之旅…
并且希望用博客来记录分享自己的每一次学习!希望自己能够坚持下去!
作为一个小白,如果有写的不好的地方希望大家能够多多指出,多多讨论,一起成长!
首先我们需要一个c#脚本来抓取屏幕图像,然而unity为我们提供了一个这样的接口:OnRenderImage函数,
定义:void OnRenderImage(RenderTexture src, RenderTexture dest)
详细说明请查看官方文档
我们先创建一个用于屏幕特效处理的基类,主要是在使用之前检查了一系列条件是否满足,以后的屏幕特效处理脚本都会继承该基类。(ps: 这里直接引用了一下《unity shader入门精要》里的脚本)
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {
// Called when start
protected void CheckResources() {
bool isSupported = CheckSupport();
if (isSupported == false) {
NotSupported();
}
}
// Called in CheckResources to check support on this platform
protected bool CheckSupport() {
if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) {
Debug.LogWarning("This platform does not support image effects or render textures.");
return false;
}
return true;
}
// Called when the platform doesn't support this effect
protected void NotSupported() {
enabled = false;
}
protected void Start() {
CheckResources();
}
// Called when need to create the material used by this effect
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
if (shader == null) {
return null;
}
if (shader.isSupported && material && material.shader == shader)
return material;
if (!shader.isSupported) {
return null;
}
else {
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
}
创建一个基础的抓取屏幕的脚本,主要通过shader处理抓取到的屏幕图像,代码如下:
using UnityEngine;
public class WaterWaveEffect_L : PostEffectsBase {
public Shader shader;
private Material _material = null;
public Material material {
get {
_material = CheckShaderAndCreateMaterial (shader, _material);
return _material;
}
}
//source:unity渲染得到的图像,destination:渲染纹理到屏幕上
void OnRenderImage (RenderTexture source, RenderTexture destination) {
//对渲染纹理的处理
Graphics.Blit (source, destination, material);
}
}
使用的时候直接把该脚本拖拽到camera上,然后把我们写好的shader拖拽到该脚本的shader属性上就可以了。
下面我们来测试一下该脚本是否正确:
首先打开unity创建一个简单的场景
然后创建一个基础的shader:
Shader "lcl/screenEffect/waterWave_L"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
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;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
float4 _MainTex_TexelSize;
fixed4 frag (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
首先我们需要让图像呈现出像水波纹一样的效果,实际上就是对uv坐标的偏移,但是我们要让它出现波纹形状,就需要用到正弦函数(sin)了,我们先来看看sin函数是什么样的(在这里推荐一个在线函数图像绘制工具),如图:
看起来是不是就像水波纹一样,现在我们知道了需要使用到的函数就好办了,我们把它应用到shader中去看看效果,下面就只贴关键代码了,如下:
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _waveLength;//波长
float _waveHeight;//振幅(波的高度)
float4 _startPos;//波的传播位置(默认中心点)
//片元着色器
fixed4 frag (v2f i) : SV_Target
{
//计算该片元到中心点的距离
float dis = distance(i.uv,_startPos);
//通过sin计算偏移
float offsetX = sin(dis * _waveLength) * _waveHeight;
i.uv.x += offsetX;
return tex2D(_MainTex, i.uv);
}
在上面我们首先通过distance函数计算出片元到中心点的距离,然后在根据正弦函数计算出x偏移值,需要注意的是在这里我们定义了三个float类型的变量,这里的都是通过c#传递过来的。c#代码如下:
//波长
[Range (0f, 100.0f)]
public float _waveLength = 38.0f;
//波纹振幅(高度)
[Range (0, 2.0f)]
public float _waveHeight = 0.5f;
private Vector4 startPos = new Vector4 (0.5f, 0.5f, 0, 0);
void OnRenderImage (RenderTexture source, RenderTexture destination) {
//设置一系列参数
material.SetVector ("_startPos", startPos);
material.SetFloat ("_waveLength", _waveLength);
material.SetFloat ("_waveHeight", _waveHeight);
//把经过shader处理后屏幕图像copy到destination中
Graphics.Blit (source, destination, material);
}
最终呈现出的效果如下:
我们还可以通过调节_waveLength,_waveHeight 参数来修改波纹形状。
下面我们给波纹限制一下范围,可以通过调节参数来修改波纹扩散的范围,代码如下:
fixed4 frag (v2f i) : SV_Target
{
//计算该片元到中心点的距离
float dis = distance(i.uv,_startPos);
//通过sin计算偏移
float offsetX = sin(dis * _waveLength) * _waveHeight;
//如果该片元不在波纹范围内 偏移设置为0
if(dis <= _currentWaveDis || dis > _currentWaveDis + _waveWidth){
offsetX = 0;
}
i.uv.x += offsetX;
return tex2D(_MainTex, i.uv);
}
运行效果如下,我们可以通过调节_currentWaveDis ,_waveWidth参数来修改波纹的范围了
现在我们可以随意的控制波纹范围了,接下来就需要让该波纹动起来!让它从里往外扩散,其实很简单,给定一个时间变量就可以了,如下:
fixed4 frag (v2f i) : SV_Target
{
//计算该片元到中心点的距离
float dis = distance(i.uv,_startPos);
//通过sin计算偏移
float offsetX = sin(dis * _waveLength) * _waveHeight;
//随着时间变化
_currentWaveDis*=_Time.y;
//如果该片元不在波纹范围内 偏移设置为0
if(dis <= _currentWaveDis || dis > _currentWaveDis + _waveWidth){
offsetX = 0;
}
i.uv.x += offsetX;
return tex2D(_MainTex, i.uv);
}
这里就不贴图。gif太麻烦。点击运行就可以看到效果了。
最后,我们需要让波纹跟随鼠标点击的位置来扩散,我们可以用c#脚本来获取鼠标点击的坐标,然后转换到uv的(0,1)区间,传递给shader,并且_currentWaveDis 变量也由c#来控制,代码如下:
c#:
//波纹速度
[Range (0f, 1.0f)]
public float waveSpeed = 0.5f;
private Vector4 startPos = new Vector4 (0.5f, 0.5f, 0, 0);
//波纹开始运动的时间
private float waveStartTime;
void OnRenderImage (RenderTexture source, RenderTexture destination) {
//波纹运动的距离
float _currentWaveDis = (Time.time - waveStartTime) * waveSpeed;
//设置一系列参数
material.SetVector ("_startPos", startPos);
material.SetFloat ("_waveLength", _waveLength);
material.SetFloat ("_waveHeight", _waveHeight);
material.SetFloat ("_waveWidth", _waveWidth);
material.SetFloat ("_currentWaveDis", _currentWaveDis);
Graphics.Blit (source, destination, material);
}
void Update () {
if (Input.GetMouseButton (0)) {
Vector2 mousePos = Input.mousePosition;
//将mousePos转化为(0,1)区间
startPos = new Vector4 (mousePos.x / Screen.width, mousePos.y / Screen.height, 0, 0);
waveStartTime = Time.time;
}
}
shader:
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _waveLength;
float _waveHeight;
float _waveWidth;
float _currentWaveDis;
float4 _startPos;
fixed4 frag (v2f i) : SV_Target
{
//计算该片元到中心点的距离
float dis = distance(i.uv,_startPos);
//通过sin计算偏移
float offsetX = sin(dis * _waveLength) * _waveHeight;
//如果该片元不在波纹范围内 偏移设置为0
if(dis <= _currentWaveDis || dis > _currentWaveDis + _waveWidth){
offsetX = 0;
}
i.uv.x += offsetX;
return tex2D(_MainTex, i.uv);
}
完整代码可以在我的 github上找到
shader脚本
c#脚本