实现
我们的老电影式的画面特效中的每一个独立图层实际都很简单,但是,当我们把它们整合在一起我们就可以得到非常震撼的效果。现在你的画面特效脚本系统应该已经建立好了,现在我们来实现具体的脚本和Shader。
首先,我们来填写脚本的主要代码。
第一步我们要定义一些需要在面板中显示的变量,以便让用户进行调整。我们可以利用之前制作原型所用的photoshop作为参考,来决定我们需要显示哪些变量。在脚本中添加如下代码:
#region Variables
public Shader oldFilmShader;
public float oldFilmEffectAmount = 1.0f;
public Color sepiaColor = Color.white;
public Texture2D vignetteTexture;
public float vignetteAmount = 1.0f;
public Texture2D scratchesTexture;
public float scratchesXSpeed;
public float scratchesYSpeed;
public Texture2D dustTexture;
public float dustXSpeed;
public float dustYSpeed;
private Material curMaterial;
private float randomValue;
#endregion
然后,我们需要填充OnRenderImage函数。在这个函数里,我们将要把上述变量传递给Shader,使得Shader可以使用这些数据来处理render texture:
void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture){
if (oldFilmShader != null) {
material.SetColor(“_SepiaColor”, sepiaColor);
material.SetFloat(“_VignetteAmount”, vignetteAmount);
material.SetFloat(“_EffectAmount”, oldFilmEffectAmount);
if (vignetteTexture) {
material.SetTexture(“_VignetteTex”, vignetteTexture);
}
if (scratchesTexture) {
material.SetTexture(“_ScratchesTex”, scratchesTexture);
material.SetFloat(“_ScratchesXSpeed”, scratchesXSpeed);
material.SetFloat(“_ScratchesYSpeed”, scratchesYSpeed);
}
if (dustTexture) {
material.SetTexture(“_DustTex”, dustTexture);
material.SetFloat(“_DustXSpeed”, dustXSpeed);
material.SetFloat(“_DustYSpeed”, dustYSpeed);
material.SetFloat(“_RandomValue”, randomValue);
}
Graphics.Blit(sourceTexture, destTexture, material);
} else {
Graphics.Blit(sourceTexture, destTexture);
}
}
最后,我们需要在Update函数中保证一些变量的范围:
void Update () {
vignetteAmount = Mathf.Clamp(vignetteAmount, 0.0f, 1.0f);
oldFilmEffectAmount = Mathf.Clamp(oldFilmEffectAmount, 0.0f, 1.0f);
randomValue = Random.Range(-1.0f, 1.0f);
}
接下来,我们来实现关键的Shader部分。
首先,我们需要创建对应的Properties。这使得脚本和Shader之间可以进行通信。在Properties块中输入如下代码:
Properties {
_MainTex (“Base (RGB)”, 2D) = “white” {}
_VignetteTex (“Vignette Texture”, 2D) = “white” {}
_VignetteAmount (“Vignette Opacity”, Range(0, 1)) = 1
_ScratchesTex (“Scraches Texture”, 2D) = “white” {}
_ScratchesXSpeed (“Scraches X Speed”, Float) = 10.0
_ScratchesYSpeed (“Scraches Y Speed”, Float) = 10.0
_DustTex (“Dust Texture”, 2D) = “white” {}
_DustXSpeed (“Dust X Speed”, Float) = 10.0
_DustYSpeed (“Dust Y Speed”, Float) = 10.0
_SepiaColor (“Sepia Color”, Color) = (1, 1, 1, 1)
_EffectAmount (“Old Film Effect Amount”, Range(0, 1)) = 1
_RandomValue (“Random Value”, Float) = 1.0
}
和往常一样,我们需要在CGPROGRAM块中添加对应的变量,以便Properties块可以和CGPROGRAM块通信:
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag
#include “UnityCG.cginc”
uniform sampler2D _MainTex;
uniform sampler2D _VignetteTex;
uniform sampler2D _ScratchesTex;
uniform sampler2D _DustTex;
fixed4 _SepiaColor;
fixed _VignetteAmount;
fixed _ScratchesXSpeed;
fixed _ScratchesYSpeed;
fixed _DustXSpeed;
fixed _DustYSpeed;
fixed _EffectAmount;
fixed _RandomValue;
现在,我们来填充最关键的frag函数,在这里我们将真正处理画面特效中的每一个像素。首先,我们来获取render texture和晕影纹理(Vignette texture):
fixed4 frag (v2f_img i) : COLOR {
half2 renderTexUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005));
fixed4 renderTex = tex2D(_MainTex, renderTexUV);
// Get teh pixed from the Vignette Texture
fixed4 vignetteTex = tex2D(_VignetteTex, i.uv);
解释:frag函数是整个特效的关键所在。和Photoshop中的图层类似,我们的Shader也是处理的每一个图层,然后把它们再结合在一起。因此,我们的分析过程也是按每个图层,你可以想象Photoshop中图层是如何工作的。这样的思维可以帮助我们将来创建新的画面特效。
这里的几行代码定义了UV坐标是如何为render texture工作的。由于我们想要模仿一个老电影的风格,我们可以在每一帧调整render texture的UV坐标,来模拟一个闪烁的效果。
第一、二行对render texture的Y方向添加了一些偏移来达到上述的闪烁效果。它使用了Unity内置的_SinTime变量,来得到一个范围在-1到1的正弦值。然后再乘以了一个很小的值0.005,来得到一个小范围的偏移(-0.005, +0.005)。最后的值又乘以了_RandomValue变量,这是我们在脚本中定义的变量,它在Update函数中被随机生成为-1到1中的某一个值,来实现上下随机弹动的效果。在得到UV坐标后,我们在第二行使用了tex2D()函数在render texture上进行采样。
最后一行很简单,直接使用tex2D()函数对晕影纹理进行采样,不需要再移动UV坐标了。
通过上述代码我们现在得到了底色(renderTex)和第一层图层(vignetteTex)。
然后,我们需要添加对灰尘(dust)和划痕(scratches)的处理。添加如下代码:
// Process the Scratches UV and pixels
half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed),
i.uv.y + (_Time.x * _ScratchesYSpeed));
fixed4 scratchesTex = tex2D(_ScratchesTex, scratchesUV);
// Process the Dust UV and pixels
half2 dustUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _DustXSpeed),
i.uv.y + (_Time.x * _DustYSpeed));
fixed4 dustTex = tex2D(_DustTex, dustUV);
解释:这些代码和上一步中的很类似,也就是我们需要生成移动后的UV坐标来修改当前图层在画面特效中的位置。我们还是使用了内置的_SinTime变量来得到一个-1到1范围内的值,再乘以我们的随机值_RandomValue,最后再乘以一个系数来调整移动的整体速度。一旦生成UV坐标后,我们就可以使用tex2D函数对灰尘纹理和划痕纹理进行采样。
通过上述代码,我们得到了第二层图层中的scratchesTex和dustTex。
然后,处理棕褐色调(Sepia Tone):
// Get the luminosity values from the render texture using the YIQ values
fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb);
// Add the constant calor to the lum values
fixed4 finalColor = lum + lerp(_SepiaColor, _SepiaColor + fixed4(0.1f, 0.1f, 0.1f, 0.1f), _RandomValue);
解释:这一步是处理老电影效果的颜色。通过上述代码,我们给整个画面染上了一种发黄的颜色。首先,我们把render texture转换到它的灰度版本(第一行)。我们使用了YIQ值中的光度值(luminosity,即Y表示的意思)来完成这个目的。YIQ值是NTSC电视系统标准使用的颜色空间。更多的关于YIQ颜色的内容可以参考文章最后的链接。这里我们只要知道,YIQ中的Y值就是任意图像的光度常量值,也就是说对任意图像我们都可以通过乘以这个常量值来得到这个图像的每个像素的光度值(luminosity)。因此,我们可以通过把render texture中的每一个像素点乘光度常量系数,来生成一个灰度图。这也就是第一行所做的事情。
一旦我们得到光度值后,我们可以简单地添加一个颜色,来得到我们想要图像锁呈现的色调。这个颜色(_SepiaColor)是脚本传递给Shader的。我们还使用了一个lerp函数,其右边界值是_SepiaColor加上一个常量后所得到的一个更亮的颜色,并且以_RandomValue作为第三个参数,来模拟一个光度上的闪烁效果。
通过上述代码,我们得到了第三层图层中的棕褐色调(暂时存储在finalColor)。
最后,我们把上述图层和颜色结合在一起,返回最终的画面颜色:
// Create a constant white color we can use to adjust opacity of effects
fixed3 constantWhite = fixed3(1, 1, 1);
// Composite together the different layers to create final Screen Effect
finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount);
finalColor.rgb *= lerp(scratchesTex, constantWhite, _RandomValue);
finalColor.rgb *= lerp(dustTex, constantWhite, (_RandomValue * _SinTime.z));
finalColor = lerp(renderTex, finalColor, _EffectAmount);
return finalColor;
解释:最后,我们把每一个图层混合在一起完成最终的画面特效。这里,我们把所有图层乘起来,就像我们在Photoshop中将所有图层乘起来一样(当然那里是使用了混合模式)。每一个图层还使用了一个lerp函数以便我们可以调整透明度。
其实这里没有非常清晰的解释,可以看出来上述lerp函数的参数很多同样使用了随机数来模拟一个闪烁弹动的效果。第一个lerp(对应vignetteTex)比较简单,我们可以通过在面板中调整Vignette Amount来调整晕影纹理的透明度。第二个lerp(对应scrachesTex)的右边界值是(1, 1, 1, 1),来模拟画面中划痕时隐时现的效果。第三个lerp(对应dustTex)的右边界同样使用了(1, 1, 1, 1),而且第三个参数还乘以了_SinTime,好吧,这里我也不知道为什么。。。最后一个lerp(对应renderTex)很好理解,此时的finalColor是所有图层相乘后得到的最终老电影效果,通过调整面板的Effect Amount可以控制画面特效的透明度。
完整的脚本和Shader如下:
OldFilmEffect脚本:
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
public class OldFilmEffect : MonoBehaviour {
#region Variables
public Shader oldFilmShader;
public float oldFilmEffectAmount = 1.0f;
public Color sepiaColor = Color.white;
public Texture2D vignetteTexture;
public float vignetteAmount = 1.0f;
public Texture2D scratchesTexture;
public float scratchesXSpeed;
public float scratchesYSpeed;
public Texture2D dustTexture;
public float dustXSpeed;
public float dustYSpeed;
private Material curMaterial;
private float randomValue;
#endregion
#region Properties
public Material material {
get {
if (curMaterial == null) {
curMaterial = new Material(oldFilmShader);
curMaterial.hideFlags = HideFlags.HideAndDontSave;
}
return curMaterial;
}
}
#endregion
// Use this for initialization
void Start () {
if (SystemInfo.supportsImageEffects == false) {
enabled = false;
return;
}
if (oldFilmShader != null && oldFilmShader.isSupported == false) {
enabled = false;
}
}
void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture){
if (oldFilmShader != null) {
material.SetColor(“_SepiaColor”, sepiaColor);
material.SetFloat(“_VignetteAmount”, vignetteAmount);
material.SetFloat(“_EffectAmount”, oldFilmEffectAmount);
if (vignetteTexture) {
material.SetTexture(“_VignetteTex”, vignetteTexture);
}
if (scratchesTexture) {
material.SetTexture(“_ScratchesTex”, scratchesTexture);
material.SetFloat(“_ScratchesXSpeed”, scratchesXSpeed);
material.SetFloat(“_ScratchesYSpeed”, scratchesYSpeed);
}
if (dustTexture) {
material.SetTexture(“_DustTex”, dustTexture);
material.SetFloat(“_DustXSpeed”, dustXSpeed);
material.SetFloat(“_DustYSpeed”, dustYSpeed);
material.SetFloat(“_RandomValue”, randomValue);
}
Graphics.Blit(sourceTexture, destTexture, material);
} else {
Graphics.Blit(sourceTexture, destTexture);
}
}
// Update is called once per frame
void Update () {
vignetteAmount = Mathf.Clamp(vignetteAmount, 0.0f, 1.0f);
oldFilmEffectAmount = Mathf.Clamp(oldFilmEffectAmount, 0.0f, 1.0f);
randomValue = Random.Range(-1.0f, 1.0f);
}
void OnDisable () {
if (curMaterial != null) {
DestroyImmediate(curMaterial);
}
}
}
OldFilmEffectShader如下:
Shader “Custom/OldFilmEffectShader” {
Properties {
_MainTex (“Base (RGB)”, 2D) = “white” {}
_VignetteTex (“Vignette Texture”, 2D) = “white” {}
_VignetteAmount (“Vignette Opacity”, Range(0, 1)) = 1
_ScratchesTex (“Scraches Texture”, 2D) = “white” {}
_ScratchesXSpeed (“Scraches X Speed”, Float) = 10.0
_ScratchesYSpeed (“Scraches Y Speed”, Float) = 10.0
_DustTex (“Dust Texture”, 2D) = “white” {}
_DustXSpeed (“Dust X Speed”, Float) = 10.0
_DustYSpeed (“Dust Y Speed”, Float) = 10.0
_SepiaColor (“Sepia Color”, Color) = (1, 1, 1, 1)
_EffectAmount (“Old Film Effect Amount”, Range(0, 1)) = 1
_RandomValue (“Random Value”, Float) = 1.0
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag
#include “UnityCG.cginc”
uniform sampler2D _MainTex;
uniform sampler2D _VignetteTex;
uniform sampler2D _ScratchesTex;
uniform sampler2D _DustTex;
fixed4 _SepiaColor;
fixed _VignetteAmount;
fixed _ScratchesXSpeed;
fixed _ScratchesYSpeed;
fixed _DustXSpeed;
fixed _DustYSpeed;
fixed _EffectAmount;
fixed _RandomValue;
fixed4 frag (v2f_img i) : COLOR {
half2 renderTexUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005));
fixed4 renderTex = tex2D(_MainTex, renderTexUV);
|