目录
- 效果图
- 理论
- 边缘检测
- 高斯模糊
- Bloom效果
- 代码
- 调整亮度饱和度对比度
- 边缘检测
- 高斯模糊
- Bloom效果
效果图
【边缘检测】
Edges Only = 0
Edges Only = 1
【高斯模糊】
Down Sample = 8,图像像素化
Blur spread = 20, 图像虚影
【Bloom效果】
得到了一只发光的喵喵
理论
屏幕后处理脚本系統:
首先在摄像中添加一个用于屏幕后处理的脚本。在这 脚本中 ,实现 OnRenderlmage 来抓取屏幕。然后再调用 Graphics.Blit 函数使用特定的 Unity Shader 来对当前图像进行处理,返回的渲染纹理显示到屏幕上。一些复杂的屏幕特效需要多次调用 Graphics.Blit。
- OnRenderlmage
MonoBehaviour .OnRenderimage (RenderTexture src, RenderTexture dest)
Unity 会把当前渲染得到的图像存储在第一 参数对应的源渲染纹理中,通过函数中的一系列操作后,再把目标渲染纹理即第二 参数对应的渲染纹理显示到屏幕上
- Grapbics.Bllt
参数 src 对应了源纹理(传递给 Shader 中名为 MainTex 的纹理属性),参数 dest 是目标渲染纹理,参数 mat 我们使用的材质,这个材质使用 Unity Shader 将会进行各种屏幕后处理操作,参数 pass 的默认值-1, 表示将会 Shader 的所有 Pass ,否则只会调用给定索引的 Pass
边缘检测
【原理】
利用一些边缘检测算子对图像进行卷积 (convolution) 操作
它们都包含了两个方向的卷积核,分别用于检测水
平方向和竖直方向上的边缘信息,整体的梯度为:
出于性能的考虑,有时会使用绝对值操作来代替开根号操作:
梯度值越大,越有可能是边缘点
【代码解析】
- 摄像机的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, 并据此创建相应的材质、提供参数、定义 OnRenderlmage 函数来进行真正的特效处理
每当 OnRenderlmage 函数被调用时 ,它会检查材质是否可用。如果可用,就把参数传递给材质,再调用 Graphics Blit 进行处理; 否则直接把原图像显示到屏幕上,不做任何处理。
- shader
状态设置: 屏幕后处理的 shader 的“标配”: ZTest Always Cull Off ZWrite Off
顶点着色器: 通常比较简单,输出Pose,uv[9]
片元着色器:调用 Sobel 函数计算当前像素的梯度值 edge,然后利用_EdgeOnly 在背景为原图纯色之间插值
关闭该 Unity Shader Fallback:
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 再使用水平方向的维高斯核对图像进行滤波,得到最终的目标图像。
- 摄像机CS脚本
继承基类、声明该效果需要的 Shader, 并据此创建相应的材质、提供了调整高斯模糊迭代次数 模糊范围和缩放系数的参数、定义 OnRenderlmage 函数来进行真正的特效处理
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, 重复上述过程。
- shader
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效果
【原理】
让画面中较亮区域“扩散”到周围的区域中,造成一种朦胧的效果
Bloom 的实现:
首先根据一个阙值提取出图像中的较亮区域 ,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果 最后再将其和原图像进行混合得到最终的效果
【代码解析】
摄像机CS脚本
流程:继承基类、声明使用的shader, 创建相应材质、材质参数、定义 OnRenderlmage 函数来进行真正的特效处理
- OnRenderImage部分
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,把高斯模糊后的较亮区域与原图混合
- shader
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
}
Bloom效果
【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 // 关闭
}