【边缘检测】
Edges Only = 0
Edges Only = 1
【高斯模糊】
Down Sample = 8,图像像素化
Blur spread = 20, 图像虚影
屏幕后处理脚本系統:
首先在摄像中添加一个用于屏幕后处理的脚本。在这 脚本中 ,实现 OnRenderlmage 来抓取屏幕。然后再调用 Graphics.Blit 函数使用特定的 Unity Shader 来对当前图像进行处理,返回的渲染纹理显示到屏幕上。一些复杂的屏幕特效需要多次调用 Graphics.Blit。
MonoBehaviour .OnRenderimage (RenderTexture src, RenderTexture dest)
Unity 会把当前渲染得到的图像存储在第一 参数对应的源渲染纹理中,通过函数中的一系列操作后,再把目标渲染纹理即第二 参数对应的渲染纹理显示到屏幕上
参数 src 对应了源纹理(传递给 Shader 中名为 MainTex 的纹理属性),参数 dest 是目标渲染纹理,参数 mat 我们使用的材质,这个材质使用 Unity Shader 将会进行各种屏幕后处理操作,参数 pass 的默认值-1, 表示将会 Shader 的所有 Pass ,否则只会调用给定索引的 Pass
【原理】
利用一些边缘检测算子对图像进行卷积 (convolution) 操作
它们都包含了两个方向的卷积核,分别用于检测水
平方向和竖直方向上的边缘信息,整体的梯度为:
出于性能的考虑,有时会使用绝对值操作来代替开根号操作:
梯度值越大,越有可能是边缘点
【代码解析】
using UnityEngine;
using System.Collections;
// 继承基类
public class EdgeDetection : PostEffectsBase {
// 声明该效果需要的 Shader, 并据此创建相应的材质:
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null;
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
// 在脚本中提供用于调整边缘线强度、描边颜色以及背景颜色的参数:
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f; // edgesOnly 值为0时,边缘将会叠加在原渲染图像上:当 dgesOnly 值为1时,则会只显示边缘
public Color edgeColor = Color.black;
// public Color backgroundColor = Color.white; 先声明,再在OnRenderImage里使用,决定了相机的控制面板,跟shader里的properties类似
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
// material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
大概的流程为:继承基类、声明该效果需要的 Shader, 并据此创建相应的材质、提供参数、定义 OnRenderlmage 函数来进行真正的特效处理
每当 OnRenderlmage 函数被调用时 ,它会检查材质是否可用。如果可用,就把参数传递给材质,再调用 Graphics Blit 进行处理; 否则直接把原图像显示到屏幕上,不做任何处理。
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_EdgeOnly ("Edge Only", Float) = 1.0 // // edgesOnly 值为0时,边缘将会叠加在原渲染图像上:当 dgesOnly 值为1时,则会只显示边缘
_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off
用于屏幕后处理的 shader 的“标配”:
ZTest Always: 无论被不被遮住,都一直绘制
ZWrite Off:为了防止它“挡住”在其后面被渲染的物体。例如,如果当前的 OnRenderlmage 函数在所有不透明的 Pass 执行完毕后立即被调用,不关闭深度写入就会影响后面透明的 Pass 的渲染。
sampler2D _MainTex;
uniform half4 _MainTex_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;
}
维数为9的纹理数组,对应了使用 Sobel 算子采样时需要的9个邻域纹理坐标
把计算采样纹理坐标的代码从FS移到VS中,可以减少运算,提高性能。
从顶点着色器到片元着色器的插值是线性的,因此这样的转移并不会影响纹理坐标的计算结果。
VS
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i); // 调用 Sobel 函数计算当前像素的梯度值 edge
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge); // 背景为原图
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge); // 背景为纯色
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); // 上面两幅图的插值
}
本例子仅仅利用了屏幕中的颜色信息,而在实际应用中,物体的纹理、阴影等信息均会影响边缘检测的结果,使得结果包含许多非预期的描边。
【原理】
均值模糊:
使用了卷积操作,卷积核中的各个元素值都相等,且相加等于1, 卷积后得到的像素值是其邻域内各个像素值的平均值。
中值模糊:
选择邻域内对所有像素排序后的中值替换掉原颜色。
高斯模糊:
邻域像素距离越近,对当前处理像素的影响程度越大
可以把二维高斯函数拆分成两个一维函数,先后对图像进行滤波,它们得到的结果和直接使用二维高斯核是的,但采样次数只需要 2xNxWxH 。同时,两个一维高斯核中包含了很多重复的权重,实际只需要记录3个权重值即可
【代码解析】
第一个 Pass 将会使用竖直方向的一维高斯核对图像进行滤波
第二个 Pass 再使用水平方向的维高斯核对图像进行滤波,得到最终的目标图像。
void OnRenderImage (RenderTexture src, RenderTexture dest) {
// 检查材质是否可用。如果可用,就把参数传递给材质
if (material != null) {
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
// 调用 Graphics.Blit 函数使用特定的 Unity Shader 来对当前图像进行处理,把返回的渲染纹理显示到屏幕上
Graphics.Blit(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); // 第一个Pass
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1); // 第二个Pass
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
迭代次数 模糊范围和缩放系数
1、BlurSize:
BlurSize越大,模糊程度越高,但采样数却不会受到影响,但过大会造成虚影
2、downSample:
声明缓冲区的大小时 使用了小于原屏幕分辨率的尺寸,downSample 越大,需要处理的像素数越少,同时也能进一步提高模糊程度过大的 downSample 可能会使图像像素化
3、迭代次数:
利用两个临时缓存在迭代之间进行交替。在迭代开始前,定义buffer0, 并把 src 中的图像缩放后存储到 buffer0 中。在迭代过程中,定义 bufferl 。
在执行第一个 Pass 时,输入 buffer0, 输出是 bufferl, 完毕后首先把 buffer0 释放,再把结果值 buffer1 存储到 buffer0 中,重新分配 bufferl, 然后再调用第二个Pass, 重复上述过程。
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
使用 CGINCLUDE 来组织代码,码不需要包含在任何 Pass 语义块中
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
VS:
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0; // 5维高斯核
};
// 计算采样纹理坐标
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv; // 当前采样纹理
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _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) {
FS:
fixed4 fragBlur(v2f i) : SV_Target {
float weight[3] = {0.4026, 0.2442, 0.0545}; // 对称性,我们只需要记录 3 个高斯权重
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; // 当前像素*权重值
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);
}
两个 Pass 共用的片元着色器
两个Pass
ZTest Always Cull Off ZWrite Off
Pass {
// 设置渲染状态,name语义
// 方便再其他Shader 中直接通过它们的名字来使用该 Pass
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
FallBack Off
【原理】
让画面中较亮区域“扩散”到周围的区域中,造成一种朦胧的效果
Bloom 的实现:
首先根据一个阙值提取出图像中的较亮区域 ,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果 最后再将其和原图像进行混合得到最终的效果
【代码解析】
摄像机CS脚本
流程:继承基类、声明使用的shader, 创建相应材质、材质参数、定义 OnRenderlmage 函数来进行真正的特效处理
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_LuminanceThreshold", luminanceThreshold); // 第一个Pass,按照一定阈值提取出较亮区域
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear; // 下采样
Graphics.Blit(src, buffer0, material, 0);
for (int i = 0; i < iterations; i++){ // iterations
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 1); // 第二个Pass
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 2); // 第三个Pass
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
material.SetTexture ("_Bloom", buffer0);
Graphics.Blit (src, dest, material, 3); // 第四个Pass, 把高斯模糊后的较亮区域与原图混合
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
Bloom 效果是建立在高斯模糊的基础上的,因此脚本中提供的参数 只增加了一个luminanceThreshold 来控制提取较亮区域时使用的阙值大小(一般情况下,图像的亮度值不会超过1,开启了HDR,精度会更高)
第一个Pass,按照一定阈值提取出较亮区域
第二个Pass,使用竖直方向的一维高斯核对图像进行滤波
第三个Pass,使用水平方向的一维高斯核对图像进行滤波
第四个Pass,把高斯模糊后的较亮区域与原图混合
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bloom ("Bloom (RGB)", 2D) = "black" {}
_LuminanceThreshold ("Luminance Threshold", Float) = 0.5
_BlurSize ("Blur Size", Float) = 1.0
}
提取较亮区域需要使用的顶点着色器和片元着色器
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);
return c * val;
}
在片元着色器中,将采样得到的亮度值减去阐值Luminance Threshold, 并把结果截取到 0-1 范围内。然后把该值和原像素值相乘,得到提取后的亮部区域
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);
}
四个Pass
ZTest Always Cull Off ZWrite Off
Pass {
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
UsePass "Custom/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL" // UsePass要大写
UsePass "Custom/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
Fallback Off
【CS】
using UnityEngine;
using System.Collections;
public class BrightnessSaturationAndContrast : PostEffectsBase{ // 继承基类
// 声明该效果需要的 Shader, 并据此创建相应的材质
public Shader briSatConShader; // 是我们指定的shder
private Material briSatConMaterial; // 是创建的材质,
public Material material{ // 我们提供了名为 material 的材质来访问 briSatConMaterial
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;
// 定义 OnRenderlmage 函数来进行真正的特效处理
void OnRenderImage(RenderTexture src, RenderTexture dest) {
// 检查材质是否可用。如果可用,就把参数传递给材质
if (material != null) {
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
// 调用 Graphics.Blit 函数使用特定的 Unity Shader 来对当前图像进行处理,把返回的渲染纹理显示到屏幕上
Graphics.Blit(src, dest, material);
} else {
// 否则直接把原图像显示到屏幕上,不做任何处理。
Graphics.Blit(src, dest);
}
}
}
【shader】
// 状态设置
// 屏幕后处理的 shader 的“标配”: ZTest Always Cull Off ZWrite Off
//
// 顶点着色器
// 通常比较简单,输出Pose,uv
//
// 关闭该 Unity Shader Fallback:
Shader "Custom/C9_BrightnessSaturationAndContrast"{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Brightness ("Brightness", Float) = 1
_Saturation("Saturation", Float) = 1
_Contrast("Contrast", Float) = 1
}
SubShader {
Pass {
// 状态设置
// 屏幕后处理的 shader 的“标配”
// 关闭了深度写入,防止它“挡住”在其后面被渲染的物体
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 在代码中访问各个属性
sampler2D _MainTex;
half _Brightness;
half _Saturation;
half _Contrast;
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;
}
fixed4 frag(v2f i) : SV_Target {
fixed4 renderTex = tex2D(_MainTex, i.uv);
// Apply brightness
fixed3 finalColor = renderTex.rgb * _Brightness;
// Apply saturation
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b; // 计算亮度值
fixed3 luminanceColor = fixed3(luminance, luminance, luminance); // 创建了一个饱和度为0的颜色值
finalColor = lerp(luminanceColor, finalColor, _Saturation); // 使用_Saturation 属性在其和上一步得到的颜色之间进行插值
// Apply contrast
fixed3 avgColor = fixed3(0.5, 0.5, 0.5); // 首先创建一个对比度为0的颜色值
finalColor = lerp(avgColor, finalColor, _Contrast); // 再使用_Contrast 属性和上一步得到的颜色之间进行插值
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
Fallback Off // 关闭
}
【CS】
using UnityEngine;
using System.Collections;
// 继承基类
public class EdgeDetection : PostEffectsBase {
// 声明该效果需要的 Shader, 并据此创建相应的材质:
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null;
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
// 在脚本中提供用于调整边缘线强度、描边颜色以及背景颜色的参数:
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f; // edgesOnly 值为0时,边缘将会叠加在原渲染图像上:当 dgesOnly 值为1时,则会只显示边缘
public Color edgeColor = Color.black;
// public Color backgroundColor = Color.white; 先声明,再在OnRenderImage里使用,决定了相机的控制面板,跟shader里的properties类似
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
// material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
【shader】
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/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 无论被不被遮住,都一直绘制
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment fragSobel
sampler2D _MainTex;
uniform half4 _MainTex_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;
// 维数为9的纹理数组,对应了使用 Sobel 算子采样时需要的9个邻域纹理坐标??
// 把计算采样纹理坐标的代码从FS移到VS中,可以减少运算,提高性能。
// 从顶点着色器到片元着色器的插值是线性的,因此这样的转移并不会影响纹理坐标的计算结果。
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) {
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1,
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]));
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}
half edge = 1 - abs(edgeX) - abs(edgeY); // edge 值越小,表明该位置越可能是一个边缘点
return edge;
}
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i); // 调用 Sobel 函数计算当前像素的梯度值 edge
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge); // 背景为原图
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge); // 背景为纯色
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); // 上面两幅图的插值
}
ENDCG
}
}
FallBack Off
}
【CS】
using UnityEngine;
using System.Collections;
public class GaussianBlur : PostEffectsBase { // 继承基类
// 声明该效果需要的 Shader, 并据此创建相应的材质:
public Shader gaussianBlurShader;
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, 5.0f)]
public float blurSpread = 0.6f;
[Range(1, 8)]
public int downSample = 2;
/// 1st edition: just apply blur
// void OnRenderImage(RenderTexture src, RenderTexture dest) {
// if (material != null) {
// int rtW = src.width;
// int rtH = src.height;
// // 高斯模糊需要调用两个 Pass, 我们需要使用一块中间缓存来存储第一个 Pass 执行完毕后得到的模糊结果
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//
// // Render the vertical pass
// Graphics.Blit(src, buffer, material, 0); // buffer
// // Render the horizontal pass
// Graphics.Blit(buffer, dest, material, 1); //
//
// RenderTexture.ReleaseTemporary(buffer); // 释放内存
// } else {
// Graphics.Blit(src, dest);
// }
// }
/// 2nd edition: scale the render texture
/// 声明缓冲区的大小时 使用了小于原屏幕分辨率的尺寸,
/// 对图像进行降采样可以减少需要处理的像素个数,提高性能,而且还可以得到更好的模糊效果。
/// 尽管 downSample 值越大,性能越好,但过大的 downSample 可能会造成图像像素化
// 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);
// }
// }
// 定义 OnRenderlmage 函数来进行真正的特效处理
/// 3rd edition: use iterations for larger blur
/// 迭代次数
/// 利用两个临时缓存在迭代之间进行交替。
/// 在迭代开始前,定义bufferO, 并把 src 中的图像缩放后存储到 bufferO 中。
/// 在迭代过程中,定义 bufferl 。
/// 在执行第一个 Pass 时,输入 bufferO, 输出是 bufferl, 完毕后首先把 bufferO 释放,再把结果值 buffer 存储到 bufferO 中,重新分配 bufferl, 然后再调用第二个Pass, 重复上述过程
///
void OnRenderImage (RenderTexture src, RenderTexture dest) {
// 检查材质是否可用。如果可用,就把参数传递给材质
if (material != null) {
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
// 调用 Graphics.Blit 函数使用特定的 Unity Shader 来对当前图像进行处理,把返回的渲染纹理显示到屏幕上
Graphics.Blit(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); // 第一个Pass
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1); // 第二个Pass
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
}
【shader】
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Gaussian Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
// 第一次用 CGINCLUDE,不需要包含Pass语义块
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0; // 5*5维高斯核可以拆分成两个大小为5维高斯核,只需要计算5个纹理坐标即可
};
// 把计算采样纹理坐标的代码从片元着色器中转移到顶点着色器中,可以减少运算,提高性能
// 从顶点若色器到片元右色器的插值是线性的,因此这样的转移并不会影响纹理坐标的计算结果。
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 和BlurSize相乘来控制采样距离。在高斯核维数不变的情况下, BlurSize 越大,模糊程度越高但采样数却不会受到影响。但过大的 BlurSize 值会造成虚影
o.uv[0] = uv; // 当前采样纹理
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _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}; // 对称性,我们只需要记录 3 个高斯权重
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; // 当前像素*权重值
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);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
// 设置了渲染状态,name语义
// 因为高斯模糊是非常常见的图像处理操作,在其他Shader 中直接通过它们的名字来使用该 Pass, 而不需要再重复编写代码NAME "GAUSSIAN_BLUR_VERTICAL"
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL" // 方便别人调用该Pass
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack Off
}
【CS】
using UnityEngine;
using System.Collections;
public class Bloom : PostEffectsBase { // 继承基类
// 声明使用的shader, 创建相应材质
public Shader bloomShader;
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)]
public float luminanceThreshold = 0.6f; // 一般情况下,图像的亮度值不会超过1,开启了HDR,精度会更高
// Bloom 效果是建立在高斯模糊的基础上的,代码差不多
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_LuminanceThreshold", luminanceThreshold); // 第一个Pass,按照一定阈值提取出较亮区域
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear; // 下采样
Graphics.Blit(src, buffer0, material, 0);
for (int i = 0; i < iterations; i++){ // iterations
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 1); // 第二个Pass
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 2); // 第三个Pass
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
material.SetTexture ("_Bloom", buffer0);
Graphics.Blit (src, dest, material, 3); // 第四个Pass, 把高斯模糊后的较亮区域与原图混合
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
}
【shader】
// 让画面中较亮 区域“扩散”到周围的区域中
// Pass1:根据一个阙值提取出图像中的较亮区域 把它们存储在一张渲染纹理中
// Pass2 3:利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果
// Pass 4:将其和原图像进行混合
Shader "Custom/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);
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);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
UsePass "Custom/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL" // UsePass要大写
UsePass "Custom/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off // 关闭
}