1.1.1 建立一个基本的屏幕后处理脚本系统
屏幕后处理,通常指的是渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效。使用这种技术可以为游戏画面添加更多的艺术效果,例如景深(Depth of Field)、运动模糊(Motion Blur)等。
Unity提供了抓取屏幕的接口——OnRenderImage函数,其声明如下:
MonoBehaviour.OnRenderImage(RenderTexture src, RenderTexture dest)
当脚本中声明此函数后,Unity会把当前渲染得到的图像存储到第一个参数对应的原渲染纹理中,通过函数一系列操作再把目标渲染纹理,即第二个参数对应的渲染纹理显示在屏幕上。在OnRenderImage函数中通常使用Graphics.Blit函数来完成对渲染纹理的处理,其有三种函数声明:
public static void Blit(Texture src, RenderTexture dest);
public static void Blit(Texture src, RenderTexture dest, Material mat, int pass = -1);
public static void Blit(Texture src, Material mat, int pass = -1);
src对应了源纹理,在屏幕后处理技术中,该参数通常是当前屏幕的渲染纹理或是上一步处理后得到的渲染纹理。参数dest是目标渲染纹理,如果它的值为null就会直接将屏幕后处理操作,而src纹理将会被传递给shader中命名为_MainTex的纹理属性,mat则是使用的材质,该材质使用的Unity Shader会进行各种屏幕后处理操作,参数pass默认为-1则是表示会依次调用shader内的所有pass,否则只会调用给定索引的pass。
OnRenderImage函数会在所有的不透明和透明的pass执行完毕后被带哦用,从而对场景中所有的游戏对象都产生影响。而有时若希望在不透明Pass执行完毕后立马调用OnRenderImage函数,从而不对透明物体产生任何影响,因此若在OnRenderImage函数前添加ImageEffectOpaque属性来实现这样的目的。
要在Unity中实现屏幕后处理效果,其过程通常如下:
[1]在摄像中添加一个用于屏幕后处理的脚本,在该脚本中将会调用OnRenderImage函数来获取当前屏幕的渲染纹理。
[2]再调用Graphics.Blit函数使用特定的Unity Shader来对当前图像进行处理,再把返回的渲染纹理显示到屏幕上。对于一些复杂的屏幕特效,则需要多次调用Graphics.Blit函数来对上一步的输出结果进行下一步处理。
[3]再进行屏幕处理前需要检查一系列条件是否满足,例如当前*台是否支持渲染纹理和屏幕特效,是否支持当前使用的Unity Shader等。从而可以创建一个用于屏幕后处理效果的基类,在实现各种屏幕特效时只需要继承自该基类,再实现派生类中不同的操作即可。
其脚本代码如下:
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
[RequireComponent (typeof(Camera))] //屏幕效果需要绑定在某个摄像机中
public class PostEffectsBase : MonoBehaviour {
// Called when start
protected void CheckResources() { //设置检查资源的函数
bool isSupported = CheckSupport(); //bool函数判断true 或 false
if (isSupported == false) { //如果其为false,则不支持
NotSupported();
}
}
// Called in CheckResources to check support on this platform
protected bool CheckSupport() { //设置检查支持的函数
if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) { //判断是否满足不支持的条件,如果不支持,则声明不支持然后返回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
//因为每一个屏幕处理效果通常都需要制定一个shader来创建一个用于处理渲染纹理的材质,从而基类中也提供了该方法
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) { //创建函数,输入值为shader文件与material材质
if (shader == null) { //如果shader为空,则返回空
return null;
}
if (shader.isSupported && material && material.shader == shader) //如果sahder是可支持的,同时材质的shader为该shader,则返回材质
return material;
if (!shader.isSupported) { //shader不支持则返回空
return null;
}
else {
material = new Material(shader); //新建材质,并把该shader赋给该材质
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
}
1.1.2 调整屏幕的亮度、饱和度和对比度
实践步骤:
[1]新建场景并去掉天空盒子
[2]将照片拖拽到场景中,并调节其位置从而使它充满整个场景(该照片的纹理类型已被设置为Sprite,从而可以直接拖到场景中)
[3]新建脚本,并把该脚本拖到摄像机中
[4]新建一个Unity Shader,并编写脚本与shader的代码
脚本代码:
using UnityEngine;
using System.Collections;
public class BrightnessSaturationAndContrast : PostEffectsBase { //继承PostEffectsBase基类
public Shader briSatConShader; //声明该效果需要的shader
private Material briSatConMaterial; //并创建相应的材质
public Material material {
get {
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial); //调用基类的函数
return briSatConMaterial; //返回该材质
}
}
[Range(0.0f, 3.0f)] //范围调节
public float brightness = 1.0f; //设置参数名称并初始化
[Range(0.0f, 3.0f)]
public float saturation = 1.0f;
[Range(0.0f, 3.0f)]
public float contrast = 1.0f;
void OnRenderImage(RenderTexture src, RenderTexture dest) { //调用OnRenderImage函数
if (material != null) { //如果材质不为空,则把参数传递给材质,若不可用,则直接把愿图像显示到屏幕上,不做任何处理
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
Graphics.Blit(src, dest, material); //调用Graphics.Blit函数
} else {
Graphics.Blit(src, dest);
}
}
}
shader代码:
Shader "Unity Shaders Book/Chapter 12/Brightness Saturation And Contrast" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Brightness ("Brightness", Float) = 1
_Saturation("Saturation", Float) = 1
_Contrast("Contrast", Float) = 1
}
SubShader {
Pass { //定义屏幕后处理的pass
屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片,为了防止它对物体产生影响,需要设置相关的渲染状态
ZTest Always Cull Off ZWrite Off //关闭深度写入为了防止挡住其他后面被渲染的物体
//如如果当前OnRenderImage函数在所有不透明的Pass执行完毕后立刻被调用,不关闭深度写入就会影响后面透明的Pass渲染,该设置可以看作是用于屏幕后处理的shader的标配
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half _Brightness; //声明对应的变量,接受该shader的脚本中也有
half _Saturation;
half _Contrast;
struct v2f {
float4 pos : SV_POSITION;
half2 uv: TEXCOORD0;
};
//使用Unity内置的appdata_img结构体作为顶点着色器的输入,其只包含图像处理时必须的顶点坐标和纹理坐标等变量
v2f vert(appdata_img v) { //定义顶点着色器,屏幕特效使用的顶点着色器通常比较简单,只需要进行必要的顶点变换,把正确的纹理坐标传递给片元着色器,以便对屏幕图像进行正确的采样
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target { //实现用于调整亮度、饱和度和对比度的片元着色器
fixed4 renderTex = tex2D(_MainTex, i.uv); //得到
// Apply brightness
fixed3 finalColor = renderTex.rgb * _Brightness; //利用 _Brightness来调整亮度,亮度调整只需要把原颜色乘以亮度洗漱即可
// Apply saturation
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b; //luminance:亮度值;通过对每个颜色分量乘以一个特定的系数再相加即可得到
fixed3 luminanceColor = fixed3(luminance, luminance, luminance); //使用该亮度值创建一个饱和度为0的颜色值
finalColor = lerp(luminanceColor, finalColor, _Saturation); //使用_Saturation和luminanceColor进行插值从而得到希望的饱和度颜色
// Apply contrast
fixed3 avgColor = fixed3(0.5, 0.5, 0.5); //创建一个对比度为0的颜色值
finalColor = lerp(avgColor, finalColor, _Contrast); //使用_Contrast和avgColor进行插值从而得到最终的额结果处理
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
Fallback Off
}
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iMmBDMjJmN1MzN5YmY5MTOhR2Y3gzM4UmZ1EWY0UDNl9CX5EzLchDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL1M3Lc9CX6MHc0RHaiojIsJye.png)
1.1.3 边缘检测
卷积:卷积指的是使用一个卷积核(kernel)对一张图像中的每个像素进行一系列操作。通过卷积,可以实现许多效果,如图像模糊、边缘检测等。
常见的边缘检测算子:
边缘检测则是:将卷积核所在的区域与边缘检测算子进行卷积,然后
根据不同Gx、Gy得到不同的数字后,按照下面公式进行计算从而得到最终的卷积核。
或者:
using UnityEngine;
using System.Collections;
public class EdgeDetection : PostEffectsBase {
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null; //初始化
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial); //edgeDetectShader是指定的shader,edgeDetectMaterial是对应的材质
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f; //边缘线强度并初始化,当其为0时,边缘将会叠加在原渲染图像上;当为1时,则智慧显示边缘,不显示原渲染图像
public Color edgeColor = Color.black; //描边颜色并初始化
public Color backgroundColor = Color.white; //背景颜色的参数并初始化
void OnRenderImage (RenderTexture src, RenderTexture dest) { //当OnRenderImage被调用时,将会检测材质是否可用,如果可用则将参数传递给材质
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly); //引号部分对应shader中的参数命名,两者需一致
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material); //调用Graphics.Blit进行处理,若不可用则直接显示原来的图像到屏幕上,不做任何处理
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Edge Detection" {
Properties { //声明所需各个属性
_MainTex ("Base (RGB)", 2D) = "white" {} //对应输入的渲染纹理
_EdgeOnly ("Edge Only", Float) = 1.0
_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment fragSobel
//为在代码中访问该属性,从而需要定义各属性对应的变量
sampler2D _MainTex;
uniform half4 _MainTex_TexelSize;//xxx_TexelSize是Unity为开发者提供的访问xxx纹理对应的每个纹素的大小;因为卷积需要对相邻区域内的纹理进行采样,因此需要利用_Main_TexelSize来计算各个相邻区域的纹理坐标
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0;
};
v2f vert(appdata_img v) { //顶点着色器中计算边缘检测所需要的纹理坐标
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
half Sobel(v2f i) { //定义一个维数为9的纹理数组,对应使用的sobel算子采样时需要的9个领域纹理坐标
const half Gx[9] = {-1, 0, 1, //定义了水*方向的卷积核Gx
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1, //定义了竖直方向的卷积核Gy
0, 0, 0,
1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it++) { //把计算采样纹理坐标的代码从片元着色器转移到顶点着色器中,可以减少运算,提高性能
texColor = luminance(tex2D(_MainTex, i.uv[it])); //依次对9个像素进行采样,计算他们的亮度值
edgeX += texColor * Gx[it]; //再与Gx和Gy对应的权重进行相乘,得到各自的梯度值
edgeY += texColor * Gy[it];
}
half edge = 1 - abs(edgeX) - abs(edgeY); //从1中减去水*方向和竖直方向的梯度值的绝对值,得到edge,edge值越小则表明该位置越可能是一个边缘点
return edge;
}
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i); //调用Soble函数计算当前像素的梯度值edge
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge); //利用该值分别计算了背景为原图的颜色值
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge); //利用该值分别计算了纯色下的颜色值
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); //利用_EdgeOnly在两者之间插值得到最终的像素值
}
ENDCG
}
}
FallBack Off
}
1.1.4 高斯模糊
模糊的实现有多种方法,例如均值模糊和中值模糊。模糊使用了卷积操作,它使用的卷积核中的每个元素值相等,且相加等于1。卷积得到的像素值是其邻域内对各个像素值的*均值,而中值模糊则是选择邻域内对所有像素排序后的中值替换掉原颜色。一个更高级的模糊方法是高斯模糊。
均值滤波计算方法:
中值滤波计算方法:
高斯滤波:
为了保证滤波后的图像不会变暗,从而需要对高斯核中的权重进行归一化。让每个权重除以所有权重的和。从而保证所有权重的和为1。
高斯房子模拟了邻域每个像素对当前处理像素的影响程度——距离越*,影响越大。
二维高斯函数可以拆分成两个一维函数,而两个一维函数中包含了许多重复的权重。从而对于一个大小为5的一维高斯核只需要记录3个权重值。
而对于5x5高斯核对原图像进行高斯模糊,将会先后调用两个Pass,第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass再使用水*方向的一维高斯核对图像进行滤波,得到最终的目标图像。在现实中还会利用图像缩放来进一步提高性能,通过调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。
实现步骤:
using UnityEngine;
using System.Collections;
public class GaussianBlur : PostEffectsBase { //继承基类
public Shader gaussianBlurShader; //声明该效果所需要的shader
private Material gaussianBlurMaterial = null; //创建相应的材质
public Material material {
get {
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
// Blur iterations - larger number means more blur.
[Range(0, 4)]
public int iterations = 3; //高斯模糊迭代次数
// Blur spread for each iteration - larger value means more blur
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f; //模糊范围,_BlueSize越大,模糊程度越大,但采样数不会受到影响,过大的_BlueSize值会造成虚影
[Range(1, 8)]
public int downSample = 2; //缩放系数的参数,downSample越大,需要处理的像素数越少,同时也可以进一步提高模糊程度,但过大的downSample会造成图像像素化
/// 1st edition: just apply blur
// void OnRenderImage(RenderTexture src, RenderTexture dest) {
// if (material != null) {
// int rtW = src.width;
// int rtH = src.height;
// 利用RenderTexture.GetTemporary函数分配了一块与屏幕图像大小相同的缓冲区,因为告诉模糊需要调用两个Pass,从而需要使用一块中间缓存来存储第一个Pass执行完毕后得到的模糊结果
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//
// // Render the vertical pass
// Graphics.Blit(src, buffer, material, 0); //第一个Pass执行完毕后得到的结果,首先利用Graphics.Blit(src, buffer, material, 0)(即竖直方向的一维高斯核进行滤波),对src进行处理,并存储到buffer中
// // Render the horizontal pass
// Graphics.Blit(buffer, dest, material, 1); //然后利用第二个pass对buffer进行处理,返回最终的屏幕图像
//
// RenderTexture.ReleaseTemporary(buffer); //调用RenderTexture.ReleaseTemporary(buffer)释放之前分配的内存
// } else {
// Graphics.Blit(src, dest);
// }
// }
/// 2nd edition: scale the render texture //在该版本中利用缩放对图像进行降采样,从而减少需要处理的像素个数,提高性能
// void OnRenderImage (RenderTexture src, RenderTexture dest) {
// if (material != null) {
// int rtW = src.width/downSample;
// int rtH = src.height/downSample;
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
// buffer.filterMode = FilterMode.Bilinear; //多了声明缓冲区的大小时,使用了小于原屏幕分辨率的尺寸,并将该临时渲染纹理的滤波模式设置为双线性,从而不仅减少需要处理的像素个数,同时更提高了性能,适当的降采样还可以得到更好的模糊效果
//
// // Render the vertical pass
// Graphics.Blit(src, buffer, material, 0);
// // Render the horizontal pass
// Graphics.Blit(buffer, dest, material, 1);
//
// RenderTexture.ReleaseTemporary(buffer);
// } else {
// Graphics.Blit(src, dest);
// }
// }
/// 3rd edition: use iterations for larger blur
void OnRenderImage (RenderTexture src, RenderTexture dest) { //考虑了高斯模糊的迭代次数
if (material != null) {
int rtW = src.width/downSample;
int rtH = src.height/downSample;
//执行第一个Pass时,输入的是buffer0,输出的时buffer1,完毕后首先把buffer0释放,把结果只buffer1存储到buffer0中,再重新分配buffer1
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0); //定义第一个缓存buffer0
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0); //把src中的图像缩放后存储到buffer0中
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);//定义第二个缓存buffer1
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1);
//第二个Pass时,输入的是buffer0,输出的是buffer1,重复以上步骤,迭代完毕后,最终存储在buffer0为最终的图像
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest); //利用Graphics.Blit(buffer0, dest)把结果显示在屏幕上
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Gaussian Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} //声明属性
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
CGINCLUDE //该代码不需要包含任何Pass语义块,在使用时只需要在Pass中直接制定需要使用的顶点着色器和片元着色器即可,类似于C++中的头文件
//因为高斯模糊中需要定义两个pass,但他们的片元着色器是一样的,从而可以避免开发者编写两个完全一样的frag函数
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
//因为可以拆分成2个大小为5的一维高斯核,从而只需要计算5个纹理坐标即可
//通过把计算采样纹理坐标的代码从片元着色器中转移到顶点着色器中,可以减少运算,提高性能
o.uv[0] = uv; //存储当前的采样纹理
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize; //剩余的四个坐标则是高斯模糊中对邻域采样时使用的纹理坐标,通过和_BlurSize相乘来控制采样距离
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
v2f vertBlurHorizontal(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
fixed4 fragBlur(v2f i) : SV_Target { //定义片元着色器
float weight[3] = {0.4026, 0.2442, 0.0545}; //weight变量:高斯权重
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; //sum初始化为当前的像素值乘以它的权重值
for (int it = 1; it < 3; it++) {
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it]; //根据对称性,进行两次迭代,每次迭代包含了两次纹理采样
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0); //返回滤波结果sum
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass { //定义高斯模糊使用的两个Pass
NAME "GAUSSIAN_BLUR_VERTICAL" //使用NAME语义定义了它们的名字,为Pass定义名字可以在shader中直接通过它们的名字来使用该Pass,而不需要重新写代码
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
1.1.5 Bloom效果
(Bloom有点像辉光效果
Bloom特效是游戏中常见的一种屏幕效果,该效果可以模拟真实摄像机的一种图像效果,它让画面中较亮的区域“扩散”到周围的区域,制造一种朦胧的效果。
Bloom的实现通过一个阈值提取出图像中的较亮区域,把它们存储到一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合,得到最终的效果。(有点像ps处理图层的方法)
using UnityEngine;
using System.Collections;
public class Bloom : PostEffectsBase { //继承基类
public Shader bloomShader; //声明该效果所需要的shader
private Material bloomMaterial = null; //创建对应的材质并初始化
public Material material {
get {
bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
return bloomMaterial;
}
}
// Blur iterations - larger number means more blur.
[Range(0, 4)]
public int iterations = 3; //定义属性并初始化
// Blur spread for each iteration - larger value means more blur
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
[Range(1, 8)]
public int downSample = 2;
[Range(0.0f, 4.0f)] //尽管在绝大多数的情况下,图像的亮度值不会超过1,但如果开启了HDR,硬件会允许把颜色值存储在一个更高精度范围的缓冲中,此时像素的亮度值可能会超过1,从而可以把luminanceThreshold的值规定在[0,4]中
public float luminanceThreshold = 0.6f; //增加luminanceThreshold来控制提取较亮的区域时使用的阈值大小
//Bloom建立在高斯模糊之上实现,从而与高斯模糊的代码差不多
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_LuminanceThreshold", luminanceThreshold); //提取图像中较亮的区域(没有像前面的高斯模糊一样一上来就直接对src进行采样)
int rtW = src.width/downSample; //采样
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0); //定义buffer0
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0, material, 0); //存储摘取得到的较亮区域到buffer0中
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 1); //竖直,模糊后的较亮区域区域将会存储在buffer1中
RenderTexture.ReleaseTemporary(buffer0); //释放buffer0
buffer0 = buffer1; //buffer1的数据存储会buffer0中
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0); //重新定义buffer1
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 2); //横向,模糊后的较亮区域区域将会存储在buffer1中
RenderTexture.ReleaseTemporary(buffer0); //释放buffer0
buffer0 = buffer1; //buffer0 等于 buffer1
}
material.SetTexture ("_Bloom", buffer0); //将buffer0传递给次啊之中的_Bloom纹理属性
Graphics.Blit (src, dest, material, 3); //将第四个pass进行最后的混合,并存储到目标渲染纹理dest中
RenderTexture.ReleaseTemporary(buffer0); //释放临时缓存
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Bloom" {
Properties { //定义属性
_MainTex ("Base (RGB)", 2D) = "white" {} //对应输入的渲染纹理
_Bloom ("Bloom (RGB)", 2D) = "black" {} //高斯模糊后较亮的区域
_LuminanceThreshold ("Luminance Threshold", Float) = 0.5 //提取较亮区域的阈值
_BlurSize ("Blur Size", Float) = 1.0 //控制不同迭代之间高斯模糊的模糊区域范围
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
//声明代码中需要使用的各个变量
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
//定义提取较亮区域需要使用的顶点着色器和片元着色器
v2f vertExtractBright(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
fixed4 fragExtractBright(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0); //采样得到的亮度值减去阈值_LuminanceThreshold,并把结果截取在0~1之间
return c * val; //把该值和愿像素相乘,得到提取后的亮度区域
}
struct v2fBloom {
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
//定义混合亮部图像和原图像时使用的顶点着色器和片元着色器
v2fBloom vertBloom(appdata_img v) {
v2fBloom o;
o.pos = UnityObjectToClipPos (v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0.0)
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
fixed4 fragBloom(v2fBloom i) : SV_Target {
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw); //定义两个纹理坐标,并存储在同一个类型为half4的变量uv中,xy对应_MainTex,即原图像的纹理坐标;zw对应_Bloom,即对应模糊后的较亮区域的纹理坐标
}
ENDCG
ZTest Always Cull Off ZWrite Off
//定义Bloom效果需要的4个pass
Pass {
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
//第二第三个pass使用上一节的高斯模糊定义的两个pass,该是通过UsePass来指明所使用的pass,而Unity内部会把shader名字全部转换为大写字母,从而使用时需要大写字母
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}
1.1.6 运动模糊
运动模糊时真实世界中的摄像机的一种效果,摄像机曝光时拍摄场景发生了变化就会产生模糊的画面。运动模糊效果可以让物体运动看起来更加真实光滑,但在计算机产生的图像中,由于不存在曝光这一物理现象,渲染出来的图像往往都是棱角分明,缺少运动模糊。
运动模糊的实现有许多种方法,[1]一种实现方法是利用一块累积缓存(accumulation buffer)来混合多张连续的图像。当物体快速移动产生多张图像后,开发者取它们之间的*均值作为最后的运动模糊图像。而该暴力的方法对性能的消耗很大,因为想要获取多张帧图意味着开发者需要在同一帧里渲染多次场景。[2]另一种应用广泛的方法是创建和使用速度缓存(velocity buffer),在该缓存中存储了各个像素当前的运动速度,然后利用
[1]把天空盒去掉后,添加物体在场景中,并新建一个脚本和shader文件
[2]把脚本挂在摄像机中,shader文件挂在脚本中
[3]可以编写一个移动脚本挂在物体上,从而可以运行查看效果
运动模糊脚本代码:
using UnityEngine;
using System.Collections;
public class MotionBlur : PostEffectsBase {
public Shader motionBlurShader;
private Material motionBlurMaterial = null;
public Material material {
get {
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
return motionBlurMaterial;
}
}
[Range(0.0f, 0.9f)] //定义运动模糊在混合图像时使用的模糊参数,并声明范围值
public float blurAmount = 0.5f;
private RenderTexture accumulationTexture; //定义一个RenderTexture类型的变量,并且保存之前图像叠加的结果
void OnDisable() {
DestroyImmediate(accumulationTexture);
}
void OnRenderImage (RenderTexture src, RenderTexture dest) { //定义运动模糊所使用的OnRenderImage函数
if (material != null) { //若材质不为空,可用,则运行下一个判断
// Create the accumulation texture
if (accumulationTexture == null || accumulationTexture.width != src.width || accumulationTexture.height != src.height) { //判断用于混合图像的accumulationTexture是否满足条件:不为空且与当前的屏幕分辨率相等
DestroyImmediate(accumulationTexture); //若不满足则需要把该变量销毁掉,并重新创建一个
accumulationTexture = new RenderTexture(src.width, src.height, 0);
accumulationTexture.hideFlags = HideFlags.HideAndDontSave; //HideAndDontSave表示该变量不会显示在Hierarchy,也不会保存到场景中
Graphics.Blit(src, accumulationTexture); //初始化该变量
}
// We are accumulating motion over frames without clear/discard
// by design, so silence any performance warnings from Unity
//得到有效accumulationTexture后,带调用accumulationTexture.MarkRestoreExpected()来表示需要进行一个渲染纹理的恢复操作
//恢复操作指发生在渲染纹理而该纹理又没有被提前清空或销毁的情况下
//在本例中每次调用OnRenderImage需要把当前帧图像和accumulationTexture中的图像混合,accumulationTexture不需要提前清空,因为保存了之前的混合结果
accumulationTexture.MarkRestoreExpected();
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
Graphics.Blit (src, accumulationTexture, material);
Graphics.Blit (accumulationTexture, dest);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 12/Motion Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} //输入的渲染纹理
_BlurAmount ("Blur Amount", Float) = 1.0 //混合图像时使用的混合系数
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex; //声明代码中需要使用的各个变量
fixed _BlurAmount;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
//顶点着色器
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
//定义两个片元着色器,一个用于更新渲染纹理的RGB通道部分,一个用于更新渲染纹理的A通道部分
fixed4 fragRGB (v2f i) : SV_Target { //RGB通道的shader对当前图像进行采样,并将其A通道设置为_BlurAmount,以便与可以使它的透明通道进行混合
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}
half4 fragA (v2f i) : SV_Target { //A通道直接返回采样结果,该只是为了维护渲染纹理的透明通道,不让其收到混合时使用的透明度值的影响
return tex2D(_MainTex, i.uv);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
Pass {
Blend One Zero
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
FallBack Off
(加了个可以用键盘控制物体移动的脚本在物体身上,可以看的比较仔细清楚)
移动代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
protected Transform m_transform;
[Range(0, 20)]
public float m_speed = 1;
// Use this for initialization
void Start()
{
m_transform = this.transform;
}
// Update is called once per frame
void Update()
{
float movev = 0;
float moveh = 0;
if (Input.GetKey(KeyCode.UpArrow))
{
movev += m_speed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.DownArrow))
{
movev -= m_speed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.LeftArrow))
{
moveh -= m_speed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.RightArrow))
{
moveh += m_speed * Time.deltaTime;
}
m_transform.Translate(new Vector3(moveh, 0, movev));
}
}
1.2 Unity shader入门精要笔记(十三)
1.2.1 获取深度和法线纹理
屏幕后处理效果都只是在屏幕颜色图像上进行各种操作来实现的,而很多时候不仅需要当前屏幕的颜色信息,还希望得到深度和法线信息。如在进行边缘检测时,直接利用颜色信息会使检测到的边缘信息受物体纹理和光照等外部因素的影响,得到许多不需要的边缘点。
而更好的方法是,开发者可以在深度纹理和法线上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染的模型信息,通过这样的方法检测出来的边缘更加可靠。
背后的原理:
深度纹理实际就是一张渲染纹理,只不过里面存储的像素值不是颜色值,而是一个高精度的深度值。由于被存储到一张纹理中,深度纹理里的深度值范围是[0,1],而且通常是非线性分布的。而里面的深度值来自于顶点变换后得到的归一化的设备坐标(Normalized Device Cooridiance, NDC)。一个模型想要最终被绘制到屏幕上,需要把它的顶点从模型空间变换到其次裁剪坐标系下,这是通过在顶点着色器中乘以MVP变换矩阵的来的。在变换的最后一步,开发者需要使用一个投影矩阵来变换顶点,而当开发者所使用的是透视投影类型的摄像机时,该投影矩阵是非线性的。
图 1.73 透视投影矩阵转换过程及结果
图 1.74 非透视投影矩阵转换过程及结果
在得到NDC后,深度纹理中的像素值可以很方便地计算得到,这些深度值对应了NDC只能够顶点坐标的z分量的值。由于NDC中z分量的范围在[-1,1],为了让这些值可以存储在一张图像中,可以使用下公式对其进行映射:
其中d对应深度纹理中的像素值,对应NDC中的z分量的值。
在Unity中,深度纹理可以直接来自真正的深度缓冲,其可以是由一个单独的Pass渲染而得,这取决于使用的渲染路径和硬件。当使用延迟渲染路径(包括遗留的延迟渲染路径)时,深度纹理理所当然可以访问到,因为延迟渲染会把这些信息渲染到G=buffer中。当无法直接获取深度缓存时,深度和法线纹理是通过一个单独的Pass渲染而得。Unity会使用着色器替换(Shader Replacement)技术选择那些渲染路径(即SubShader的RenderType标签)为Opaque的物体,判断他们使用的渲染队列是否小玉等于2500(内置的Background、Geometry和AlphaTest渲染队列均在此范围内),如果满足条件,就把它渲染到深度和法线纹理中。因此若想要让物体能够出现在深度和法线纹理中,需要在Shader中设置正确的RenderType标签。
在Unity中,可以选择让一个摄像机生成一个深度纹理或是一张深度+法线纹理。当选择前者,只需要一张单独的深度纹理,Unity会直接获取深度缓存或是着色器替换技术,选取需要的不透明物体,并使用它投射阴影时使用的Pass(即LightMode被设置为ShadowCaster的Pass)来得到深度纹理。而如果shader中不包含这一个Pass,那么该物体将不会出现在深度纹理中,也不能向其他物体投射影。
如果选择生成一张深度+法线纹理,Unity会创建一张和屏幕分辨率相同、精度为32位(每个通道位8位)的纹理,其中观察空间下的法线信息会被编码进纹理的R和G通道,深度信息被编码进B和A通道。法线信息的获取在延迟渲染中可以非常容易得到,Unity只需要合并深度和法线缓存即可。而在前向渲染中,默认情况下不会创建法线缓存,从而unity使用一个单独的pass把整个场景再次渲染一遍来完成。
如何获得:
通过脚本中设置摄像机的depthTextureMode来完成,然后再通过shader中直接访问特点的纹理属性即可:
camera.depthTextureMode = DepthTextureMode.Depth;
设置好摄像机模式后,在shader中通过_CameraDepthTexture变量来访问。
而深度+法线纹理,只需要:
camera.depthTextureMode = DepthTextureMode.DepthNormals
在shader中声明_CameraDepthNormalsTexture变量来访问。
而通过以下组合可以让摄像机同时产生一张深度和深度+法线纹理:
camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;
在访问到深度纹理_CameraDepthTexture后,可以使用当前像素的纹理坐标对它进行采样,而大多数情况下直接利用tex2D函数采样即可,但某些*台如PS3和PSP2则需要特殊处理。从而为处理这些*台差异,可以在shader中利用宏函数:SAMPLE_DEPTH_TEXTURE来对深度纹理进行采样:
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture , i.uv);
i.uv时float2类型,对应当前像素的纹理坐标。
而SAMPLE_DEPTH_TEXTURE_PROJ同样接受两个参数——深度纹理和一个float3和float4类型的纹理坐标,其第二个参数是由顶点着色器输出插值而得的屏幕坐标,如:
float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture , UNITY_PROJ_COORD(i.scrPos));
i.srcPos是在顶点着色器中通过调用ComputeScreenPos(o.pos)得到的屏幕坐标。
而Unity提供了计算过程,LinearEyeDepth负责把深度纹理的采样结果转换到视角空间下的深度值,Linear01Depth负责返回一个[0,1]的线性深度值,即。而两个函数内部使用了内置的_ZBufferParams变量来得到远*裁剪屏幕的距离。
查看深度和法线纹理:
通过帧调节器可以查看生成的深度和法线纹理,以便对shader进行调试。
1.2.2 再谈运动模糊
速度映射图存储了每个像素的速度,然后使用这个速度来决定模糊的方向和大小。速度缓冲的生成有多种,一种方法是把场景中所有物体的速度渲染到一张纹理中,但其缺点是需要修改场景中所有物体的shader代码,使其添加计算速度的代码并输出到一个渲染纹理中。
生成速度映射图的方法:利用深度纹理在片元着色器中为每个像素计算其在世界空间下的位置,这是通过使用当前的视角*投影矩阵的逆矩阵队NDC下的顶点坐标进行变换得到的。当得到世界空间中的顶点坐标后,就使用前一帧的视角*投影矩阵队其进行变换,得到该位置在前一帧中的NDC坐标。然后计算前一帧和当前帧的位置差,生成该像素的速度。该方法的优点是可以在一个屏幕后处理步骤中完成整个效果的模拟,缺点为需要在片元着色器中进行两次矩阵乘法的操作,对性能有所影响。
[注]:需要摄像机移动才可以看到运动模糊,若物体移动是看不到效果的
[1]创建物体场景后,把脚本挂在摄像机,然后给脚本添加对应的shader
[2]给shader和脚本编码
using UnityEngine;
using System.Collections;
public class MotionBlurWithDepthTexture : PostEffectsBase { //继承父类
public Shader motionBlurShader; //声明所需shader
private Material motionBlurMaterial = null; //建立所需材质,并初始化
public Material material {
get {
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial); //将shader赋给创建的材质
return motionBlurMaterial;
}
}
private Camera myCamera; //创建对应的摄像机
public Camera camera {
get {
if (myCamera == null) {
myCamera = GetComponent<Camera>();//获取该脚本所在的摄像机组建
}
return myCamera;
}
}
[Range(0.0f, 1.0f)]
public float blurSize = 0.5f; //创建模糊时模糊图像使用的大小,并初始化
private Matrix4x4 previousViewProjectionMatrix; //定义一个变量来保存上一帧摄像机的视角*投影矩阵
void OnEnable() {
camera.depthTextureMode |= DepthTextureMode.Depth; //获取摄像机的深度纹理
previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix; //设置摄像机的状态
}
void OnRenderImage (RenderTexture src, RenderTexture dest) { //实现OnRenderImage函数(数字图像处理,处理图片最终效果的函数)
if (material != null) {
material.SetFloat("_BlurSize", blurSize); //将blurSize值赋给shader中的_BlurSize,让其接收
material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix); //设置矩阵参数并赋给shader中对应的参数
Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix; //调用camera.projectionMatrix和camera.worldToCameraMatrix分别获得当前摄像机的视角矩阵和投影矩阵
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse; //对其相乘后取逆得到当前帧的视角*投影矩阵的逆矩阵
material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix); //将该矩阵传递给shader中对应的变量
previousViewProjectionMatrix = currentViewProjectionMatrix; //将未取逆的结果存储到previousViewProjectionMatrix以便与接下来调用到shader中
Graphics.Blit (src, dest, material); //若有material,则混合;若无则输出原图
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 13/Motion Blur With Depth Texture" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} //声明输入的渲染纹理
_BlurSize ("Blur Size", Float) = 1.0 //声明脚本中模糊图像的控制量
//而因为Unity没有提供矩阵的属性,从而在脚本中定义了,然后把脚本中的参数传递进shader中
}
SubShader {
CGINCLUDE //从而避免重复编写一样的片元着色器
#include "UnityCG.cginc"
sampler2D _MainTex; //定义纹理
half4 _MainTex_TexelSize; //定义纹理的大小变换参数,使用该变量来对深度纹理的采样坐标进行*台差异化处理
sampler2D _CameraDepthTexture; //定义摄像机的深度纹理
float4x4 _CurrentViewProjectionInverseMatrix; //定义摄像机投影*视角矩阵的逆矩阵
float4x4 _PreviousViewProjectionMatrix; //定义摄像机投影*视角矩阵,这两个矩阵由脚本传递过来
half _BlurSize; //定义模糊图像的控制量
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv_depth : TEXCOORD1; //对深度纹理采样的纹理坐标变量
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
//因为要同时处理多张渲染纹理
#if UNITY_UV_STARTS_AT_TOP //在Directx*台上需要处理图像反转问题(因为在OpenGL中远点在左下角,而directx是在左上角)
if (_MainTex_TexelSize.y < 0) //如果纹理的y<0
o.uv_depth.y = 1 - o.uv_depth.y; //翻转
#endif
return o;
}
fixed4 frag(v2f i) : SV_Target { //定义片元着色器
// Get the depth buffer value at this pixel.
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth); //对深度纹理进行采样
// H is the viewport position at this pixel in the range -1 to 1.
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1); //d是NDC坐标映射而来,若要构建像素的NDC坐标H,需要把这个深度值重新映射回NDC,使用原映射的反函数
// Transform by the view-projection inverse.
float4 D = mul(_CurrentViewProjectionInverseMatrix, H); //使用当前帧的视角*投影矩阵的逆矩阵对其进行变换
// Divide by w to get the world position.
float4 worldPos = D / D.w; //将结果除以w分量得到世界空间下的坐标表示worldPos
//得到世界坐标下的坐标
// Current viewport position
float4 currentPos = H;
// Use the world position, and transform by the previous view-projection matrix.
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos); //使用前一帧的视角*投影矩阵对它进行变换,得到前一帧在NDC下的坐标previousPos
// Convert to nonhomogeneous points [-1,1] by dividing by w.
previousPos /= previousPos.w; //除以w得到前一帧世界空间下的坐标,表示previousPos
// Use this frame's position and last frame's to compute the pixel velocity.
float2 velocity = (currentPos.xy - previousPos.xy)/2.0f; //得到该像素的速度velocity
float2 uv = i.uv;
float4 c = tex2D(_MainTex, uv);
uv += velocity * _BlurSize; //使用该速度值对它的邻域像素进行采样,相加后取*均值
for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
float4 currentColor = tex2D(_MainTex, uv);
c += currentColor;
}
c /= 3;
return fixed4(c.rgb, 1.0);
}
ENDCG
//定义模糊所需要的Pass
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}
1.2.3 全局雾效
雾效(Fog)是游戏里经常使用的一种效果。Unity内置的雾效可以产生基于距离的线性或指数雾效。而想在编写的顶点/片元着色器中实现雾效,开发者需要在shader中添加#pragma multi_compole_fog指令,同时还需要使用相关的内置宏,例如UNITY_FOG_COORDS、UNITY_TRANSFER_FOG和UNITY_APPLY_FOG等。该方法的缺点在于,开发者不仅需要为场景中所有物体添加相关的渲染代码,而且能够实现的效果也非常有限。当开发者需要对雾效进行一些个性化操作时,例如使用基于高度的雾效等,即您使用Unity内置的雾效变得不可行。
而基于屏幕后处理的全局雾效可以方便地模拟各种雾效,例如均匀的雾效、基于距离的线性/指数雾效、基于高度的雾效等。而其关键是,根据深度纹理来重建各个像素在世界空间下的位置。而在模拟运动模糊时已经实现了这个要求,即构建出当前像素的NDC坐标,再通过当前摄像机的视角*投影矩阵的逆矩阵来得到世界空间下的像素坐标,但是这样实现需要在片元着色器中进行矩阵乘法的操作,这通常会影响游戏的性能。
而有一个方法可以快速从深度纹理中重建世界坐标的方法,该方法首先对图像空间下的视锥体射线(从摄像机出发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。然后把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。在得到世界坐标后,可以通过各个公式来模拟全局雾效。
重建世界坐标:
该像素的世界坐标计算:
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
(worldPos是该像素的世界坐标,_WorldSpaceCameraPos是摄像机的空间位置坐标,linearDepth是深度纹理得到的线性深度值,interpolatedRay是由顶点着色器输出并插值后得到的射线,它不仅包含了该像素到摄像机的方向,也包含了距离信息。)
TL = camera.forward * Near + toTop – toRight
TR = camera.forward * Near + toTop + toRight
BL = camera.forward * Near - toTop – toRight
BR = camera.forward * Near + toTop + toRight
而现所得的线性深度值并非是点到摄像机的欧式距离,而是在z方向上的距离,从而不能直接使用深度值和4个角的单位方向的乘积来计算它们到摄像机的偏移量。(即我们现在所得到的只是z方向的距离,而不是眼睛实际上到该物体的距离(即所谓的深度值))。
而根据相似三角形可得到:
而因为TL与其他三个角相互对称的,而3个向量的模和TL相等,从而可以设数值:
该幕后处理的原理是使用特定的材质去渲染一个刚好填充整个屏幕的四边形面片,这个四边形面片的4个顶点就对应了*裁剪*面的4个角。因此可以把上面的计算结果传递给顶点着色器,顶点着色器根据当前的位置选择它所对应的向量,然后再将它输出,经插值后传递给片元着色器得到interpolatedRay。
雾的计算:
在简单的雾效实现中,需要计算一个雾效系数f,作为混合原始颜色和雾的颜色的混合系数:
float3 afterFog = f * fogColor + (1 - f ) * origColor;
这个雾效系数f有许多计算方法,在Unity内置的雾效实现中,支持三种雾的计算方式——线性(Linear)、指数(Exponential)、指数的*方(Exponential Squared)。当给定距离z后,f的计算公式分别如下:
Linear:
dmax为受雾影响的最大距离,dmin为受雾影响的最小距离
Exponential:
d是控制雾的浓度的参数
Exponential Squared:
[1]创建基本场景,给摄像机添加运动脚本和制造雾的脚本和shader
[2]添加对应的代码,可以看到随着摄像机向前移动,本来看不清楚的物体的部分随着前进可以看得清
雾脚本代码:
using UnityEngine;
using System.Collections;
public class FogWithDepthTexture : PostEffectsBase {
public Shader fogShader;
private Material fogMaterial = null;
public Material material {
get {
fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
return fogMaterial;
}
}
private Camera myCamera; //存储camera组件
public Camera camera {
get {
if (myCamera == null) {
myCamera = GetComponent<Camera>();
}
return myCamera;
}
}
private Transform myCameraTransform; //创建transform组件
public Transform cameraTransform {
get {
if (myCameraTransform == null) {
myCameraTransform = camera.transform;
}
return myCameraTransform;
}
}
[Range(0.0f, 3.0f)]
public float fogDensity = 1.0f; //控制雾的浓度
public Color fogColor = Color.white;
//fogstart 和 fogend相差越远则越稀疏,若相差越小,则越厚,线条越硬朗
public float fogStart = 0.0f; //雾的起始浓度
public float fogEnd = 2.0f; //雾的终止高度
void OnEnable() {
camera.depthTextureMode |= DepthTextureMode.Depth; //深度纹理
}
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
Matrix4x4 frustumCorners = Matrix4x4.identity; //计算*裁剪*面的四个角对应的向量,并存储到一个矩阵类型的变量frustumCorners
float fov = camera.fieldOfView; //camera的fov
float near = camera.nearClipPlane; //camera的最*的距离
float aspect = camera.aspect; //camera的纵横比
//公式的计算过程
float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = cameraTransform.right * halfHeight * aspect;
Vector3 toTop = cameraTransform.up * halfHeight;
Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
float scale = topLeft.magnitude / near;
topLeft.Normalize();
topLeft *= scale;
//计算各点的欧式距离
Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
topRight.Normalize();
topRight *= scale;
Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
bottomLeft.Normalize();
bottomLeft *= scale;
Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
bottomRight.Normalize();
bottomRight *= scale;
frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);
material.SetMatrix("_FrustumCornersRay", frustumCorners);
material.SetFloat("_FogDensity", fogDensity);
material.SetColor("_FogColor", fogColor);
material.SetFloat("_FogStart", fogStart);
material.SetFloat("_FogEnd", fogEnd);
Graphics.Blit (src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 13/Fog With Depth Texture" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_FogDensity ("Fog Density", Float) = 1.0
_FogColor ("Fog Color", Color) = (1, 1, 1, 1)
_FogStart ("Fog Start", Float) = 0.0
_FogEnd ("Fog End", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
float4x4 _FrustumCornersRay;
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
half _FogDensity;
fixed4 _FogColor;
float _FogStart;
float _FogEnd;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv_depth : TEXCOORD1;
float4 interpolatedRay : TEXCOORD2;
};
v2f vert(appdata_img v) { //定义顶点着色器
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
int index = 0;
if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
index = 0;
} else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
index = 1;
} else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
index = 2;
} else {
index = 3;
}
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
index = 3 - index;
#endif
o.interpolatedRay = _FrustumCornersRay[index]; //利用索引值来获取_FrustumCornersRay
return o;
}
fixed4 frag(v2f i) : SV_Target { //片元着色器产生雾效
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
fogDensity = saturate(fogDensity * _FogDensity);
fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
return finalColor;
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}
1.2.4 再谈边缘检测
在上一节中所介绍的通过sobel算子对屏幕图像进行边缘检测,从而实现描边效果,但是很多时候直接利用颜色信息进行边缘检测的方法会产生很多开发者所不希望得到的边缘线(因为sobel算子的检测原理是通过如果颜色相差太大则勾选出该线条,从而很多时候如果一个物体的贴图如果有过多凹凸不*的效果也会被勾选出来)。
而本节所介绍的是如何在深度和法线纹理上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息(即其外轮廓)。
而本节所使用Roberts算子来进行边缘检测,其卷积核如下图:
Roberts算子本质是通过计算左上角和右下角的差值,乘以右上角和左下角的差值作为评估边缘的依据。而在下面实现中,通过该方式取对角方向的深度或法线值,比较它们之间的差值,如果超过某个阈值(可由参数控制),则认为它们之间存在一条边。
[1]创建场景,给摄像机挂靠脚本与对应的shader到脚本中
[2]查看效果,不过锯齿有点严重,可以考虑进行抗锯齿操作
using UnityEngine;
using System.Collections;
public class EdgeDetectNormalsAndDepth : PostEffectsBase { //继承父集
public Shader edgeDetectShader; //创建shader选项和其对应的material
private Material edgeDetectMaterial = null;
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f; //背景颜色的参数
public Color edgeColor = Color.black; //描边颜色
public Color backgroundColor = Color.white; //背景颜色
public float sampleDistance = 1.0f; //用于控制对深度+法线纹理采样时,使用的采样距离,即视觉看为描边粗细程度
public float sensitivityDepth = 1.0f; //depth和normal则影响当邻域的深度值或法线值相差多少时被认为存在一条边街,而如果灵敏度跳的很大,那么可能是深度或法线上很小的变化也会成为一条边
public float sensitivityNormals = 1.0f;
void OnEnable() {
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals; //因为需要摄像机的深度+法线纹理,所以需要调整对应的摄像机状态
}
[ImageEffectOpaque]
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
material.SetFloat("_SampleDistance", sampleDistance);
material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader "Unity Shaders Book/Chapter 13/Edge Detection Normals And Depth" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_EdgeOnly ("Edge Only", Float) = 1.0
_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
_SampleDistance ("Sample Distance", Float) = 1.0
_Sensitivity ("Sensitivity", Vector) = (1, 1, 1, 1)
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize; //声明存储纹理大小的变量
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
float _SampleDistance; //控制采样距离
half4 _Sensitivity;
sampler2D _CameraDepthNormalsTexture;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0; //定义维数为5的纹理坐标数组,第一个坐标存储了屏幕颜色图像的采样纹理(有时候因为*台的差异从而需要对它的竖直方向进行翻转),剩下四个存储使用Roberts算子时需要采样的纹理坐标
};
v2f vert(appdata_img v) { //顶点着色器
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
#if UNITY_UV_STARTS_AT_TOP //解决*台差异性
if (_MainTex_TexelSize.y < 0)
uv.y = 1 - uv.y;
#endif
//计算罗伯特算子
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;
return o;
}
half CheckSame(half4 center, half4 sample) { //定义checksame函数,其返回值要不为0,要不为1;若为0则表明两点之间存在一条边界,若为1则不在一条边界
//得到两个点的法线和深度值
half2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
half2 sampleNormal = sample.xy;
float sampleDepth = DecodeFloatRG(sample.zw);
// difference in normals
// do not bother decoding normals - there's no need here
half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x; //两个采样点的对应值相减并取绝对值在乘以灵敏度参数
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1; //把差异值的每个分量相加再和阈值相比,如果它们的和小于阈值则返回1,否则则返回0
// difference in depth
float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
// scale the required threshold by the distance
int isSameDepth = diffDepth < 0.1 * centerDepth;
// return:
// 1 - if normals and depth are similar enough
// 0 - otherwise
return isSameNormal * isSameDepth ? 1.0 : 0.0; //检查结果相乘,作为返还值
}
fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target { //片元着色器
//使用4个纹理坐标对深度+法线纹理进行法线采样
half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
half edge = 1.0;
edge *= CheckSame(sample1, sample2); //调用checksame计算对角线上两个纹理值的差值,若返回0则两点间存在一条边界,反之则为1
edge *= CheckSame(sample3, sample4);
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
//定义边缘检测所需要的pass
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRobertsCrossDepthAndNormal //该片元着色器需要与上述所需定义的片元着色器名字对应上
ENDCG
}
}
FallBack Off
}