1.后处理基类
//屏幕后处理,顾名思义,通常指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效。
//基类的作用有二:检测平台是否支持后处理效果,及创建一个用于处理渲染纹理的材质
///
/// 屏幕后处理效果基类
///
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]//使屏幕后处理效果在编辑窗口也生效
[RequireComponent(typeof(Camera))]//首先,所有屏幕后处理效果都需要绑定在某个摄像机上
public class PostEffectsBase : MonoBehaviour
{
///
/// 检测资源,如果不支持,关闭脚本活动
///
protected void Start()
{
if (CheckSupport() == false)
enabled = false;
}
///
/// 检测平台是否支持图片渲染
///
///
protected bool CheckSupport()
{
if (SystemInfo.supportsImageEffects == false)
{
Debug.LogWarning("This platform does not support image effects or render textures.");
return false;
}
return true;
}
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material)
// 指定一个Shader来创建一个用于处理渲染纹理的材质
//第一个参数指定了该特效需要使用的Shader,第二个参数则是用于后期处理的材质。
{
if (shader == null || !shader.isSupported)
return null;
if (material && material.shader == shader)
return material;
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
return material;
}
}
2.饱和度对比度亮度调节后处理效果
(1)c#(挂载到相机)
//摄像机脚本
using UnityEngine;
using System.Collections;
public class BrightnessSaturation : PostEffectsBase//继承之前创建的基类
{
//声明需要的shder并创建相应材质(也可以在基类里写这些)
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;
//Unity为我们提供了这样一个方便的接口——OnRenderImage函数
//当我们在脚本中声明此函数后,Unity会把当前渲染得到的图像存储在第一个参数对应的源渲染纹理src中,即未处理的屏幕截屏
//通过函数中的一系列操作后,再把目标渲染纹理,即第二个参数dest对应的渲染纹理显示到屏幕上。
//处理src成为dest的函数,是Graphics.Blit函数。
void OnRenderImage(RenderTexture src, RenderTexture dest)//当前渲染得到的图像(处理前),目标渲染纹理(处理后)
{
if (material != null)
//检查材质是否可用。如果可用,就把参数传递给材质,再调用Graphics.Blit进行处理;否则,直接把原图像显示到屏幕上,不做任何处理。
{
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
Graphics.Blit(src, dest, material);//我们通常是利用Graphics.Blit函数来完成对渲染纹理的处理。
//参数src对应了源纹理,在屏幕后处理技术中,这个参数通常就是当前屏幕的渲染纹理或是上一步处理后得到的渲染纹理。
//src纹理将会被传递给Shader中名为_MainTex的纹理属性。
//参数dest是目标渲染纹理,如果它的值为null就会直接将结果显示在屏幕上。
//参数mat是我们使用的材质,这个材质使用的Unity Shader将会进行各种屏幕后处理操作
//参数pass的默认值为-1,表示将会依次调用Shader内的所有Pass。否则,只会调用给定索引的Pass。
}
else
{
Graphics.Blit(src, dest);//材质不可用。直接把原图像显示到屏幕上,不做任何处理。
}
}
}
(2)shader
//饱和度等改变
Shader "Custom/Brightness Saturation And Contrast" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}//Graphics.Blit(src, dest, material)将把第一个参数传递给Shader中名为_MainTex的属性
//下面调节的参数都通过摄像机脚本获得(material.SetFloat("_Brightness", brightness);
_Brightness ("Brightness", Float) = 1
_Saturation("Saturation", Float) = 1
_Contrast("Contrast", Float) = 1
}
SubShader {
Pass {
ZTest Always
Cull Off
ZWrite Off
//屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片,为了防止它对其他物体产生影响,我们需要设置相关的渲染状态。
//关闭了深度写入,是为了防止它“挡住”在其后面被渲染的物体
//例如,如果当前的OnRenderImage函数在所有不透明的Pass执行完毕后立即被调用,不关闭深度写入就会影响后面透明的Pass的渲染
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half _Brightness;
half _Saturation;
half _Contrast;
//struct a2v...
//使用了Unity内置的appdata_img结构体作为顶点着色器的输入,它只包含了图像处理时必需的顶点坐标和纹理坐标等变量。
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); //对截取的屏幕原图像采样
//亮度
fixed3 finalColor = renderTex.rgb * _Brightness;
//调整亮度,原颜色乘以亮度系数_Brightness
//饱和度
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属性在其和饱和度为0的颜色值之间进行插值
//对比度
fixed3 avgColor = fixed3(0.5, 0.5, 0.5); //创建一个对比度为0的颜色值(各分量均为0.5)
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
Fallback Off
}
3.描边后处理效果(风格化)
(1)c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class A_EdgeDetection : PostEffectsBase
{
public Shader briSatConShader;
private Material briSatConMaterial;
public Material material
{
get
{
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
return briSatConMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f;//边缘线强度
public Color edgeColor = Color.black;//描边颜色
public Color backgroundColor= Color.white;//背景颜色
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
material.SetFloat("_EdgesOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material);//我们通常是利用Graphics.Blit函数来完成对渲染纹理的处理。
}
else
{
Graphics.Blit(src, dest);//材质不可用。直接把原图像显示到屏幕上,不做任何处理。
}
}
}
(2)shader
//边缘检测
//如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,我们就会认为它们之间应该有一条边界。
//这种相邻像素之间的差值可以用梯度(gradient)来表示,可以想象得到,边缘处的梯度绝对值会比较大。
//卷积操作的实质在于,对于图像中的每个像素与其周围的像素进行的重新融合计算行为,以得到不同的像素处理效果
//下面选取3*3的卷积核。即选取中心图像点周围一共9个像素,利用卷积核计算出横向 和 纵向 的梯度值。从而判断出边缘。
//横向的梯度值检测出来的是纵向的边缘线,纵向的梯度值检测出来的是横向的边缘线。
Shader "Custom/15-1"
{
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纹理对应的每个纹素的大小。即纹理中的单像素的尺寸。这里的纹理指屏幕截屏。
//由于卷积需要对相邻区域内的纹理进行采样,因此我们需要利用_MainTex_TexelSize来计算各个相邻区域的纹理坐标。
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0;//维度为9的纹理数组。对应采样时需要的9个纹理坐标
};
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);
//uv指中心像素点坐标,_MainTex_TexelSize.xy * half2(-1, -1)等是取样中心周围点的操作
//计算周围像素的纹理坐标位置,其中4为原始点,
//v2f结构体中定义了一个维数为9的纹理数组,对应了使用Sobel算子采样时需要的9个邻域纹理坐标。
//通过把计算采样纹理坐标的代码从片元着色器中转移到顶点着色器中,可以减少运算,提高性能。
return o;
}
fixed luminance(fixed4 color) {// 图像置灰的方法.方便我们计算梯度值。
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
half Sobel(v2f f) {//Sobel函数将利用Sobel算子对原图进行边缘检测,得出邻接像素梯度值
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 i = 0; i < 9; i++) {
//我们依次对9个像素进行采样并置灰,再与卷积核Gx和Gy中对应的权重相乘后,叠加到各自的梯度值上。
texColor = luminance(tex2D(_MainTex, f.uv[i]));
edgeX += texColor * Gx[i];//计算横向梯度值
edgeY += texColor * Gy[i];//纵向
}
half edge = 1 - abs(edgeX) - abs(edgeY);
//从1中减去水平方向和竖直方向的梯度值的绝对值,得到edge。
//edge值越小,表明该位置越可能是一个边缘点。边缘点abs(edgeX) + abs(edgeY)的值很大
return edge;
}
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i);
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), saturate(edge));
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, saturate(edge));
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
}
}
FallBack Off
}
4.高斯模糊后处理效果
(1)c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class B_GaussianBlur : PostEffectsBase
{
public Shader gaussianBlurShader;
private Material gaussianBlurMaterial = null;
public Material material
{
get
{
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
[Range(0, 4)]
public int iterations = 3;
//高斯模糊迭代次数
//此值越大,则模糊操作的迭代次数越多,模糊效果越好,但消耗越大
[Range(0.2f, 3.0f)]
public float blurSize = 0.6f;
//模糊范围
//进行高斯模糊时,相邻像素点的间隔。此值越大相邻像素间隔越远,图像越模糊。但过大的值会导致失真。
[Range(1, 8)]
public int downSample = 2;
//降采样次数
//降采样(Downsample)也称下采样(Subsample),按字面意思理解即是降低采样频率
//对于一幅N* M的图像来说,如果降采样系数为k,则降采样即是在原图中每行每列每隔k个点取一个点组成一幅图像的一个过程。
//不难得出,降采样系数K值越大,则需要处理的像素点越少,运行速度越快。
//过大的downSample可能会造成图像像素化。
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
//material.SetFloat("_BlurSize", blurSize);疑问点
int rtW = src.width / downSample;//降采样
int rtH = src.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
//分配一块与屏幕大小相同的缓冲区(临时渲染纹理),以达到高斯模糊反复迭代的目的。
buffer0.filterMode = FilterMode.Bilinear; //将该临时渲染纹理( buffer0)的滤波模式设置为双线性,使降采样有效
Graphics.Blit(src, buffer0);//将原图拷贝到第一个临时渲染纹理
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSize);//设置shader里的参数blurSize
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//分配第二块与屏幕大小相同的缓冲区(临时渲染纹理),以达到高斯模糊反复迭代的目的。
Graphics.Blit(buffer0, buffer1, material, 0);//第一次高斯模糊,使用shader里第一个pass(竖直方向高斯核进行滤波)
RenderTexture.ReleaseTemporary(buffer0);//释放buffer0空间,因为操作后结果已经存储到buffer1了
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);//相当于刷新buffer1
Graphics.Blit(buffer0, buffer1, material, 1);//第二次高斯模糊,使用shader里第二个pass(水平方向高斯核进行滤波)
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(src, dest);
}
}
//考虑了高斯模糊的迭代次数.显示了如何利用两个临时缓存在迭代之间进行交替的过程
}
(2)shader
//高斯模糊
//处理模糊时,一般有均值模糊和高斯模糊
//均值模糊就是对周围像素取平均值(中心点周围点的权值相同)
//高斯模糊周围像素的权值是通过高斯公式推导出的
//对于图像的处理高斯正态分布是二维的,如果按照正常情况下计算的话,该算法的计算时间复杂度非常高
//假设屏幕分辨率是MN,我们的高斯核大小是mn,那么进行一次后处理的时间复杂度为O(MNmn)
//我们可以把拆分为两个一维高斯公式来处理,即横线和纵向分别做处理.时间复杂度就是O((M+N)mn)
Shader "MyShader/GaussianBlur"
{
Properties
{
_MainTex("Base (RGB)", 2D) = "white" {}
_BlurSize("Blur Size", Float) = 1.0 // 模糊采样距离,过大会产生虚影
}
SubShader
{
CGINCLUDE
//CGINCLUDE类似于C++中头文件的功能。这是我们的第一次使用。
//由于高斯模糊需要定义两个Pass,但它们使用的片元着色器代码是完全相同的,使用CGINCLUDE可以避免我们编写两个完全一样的frag函数。
#include "UnityCG.cginc"
//注意,下面的代码暂时不是pass
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
//竖直方向的顶点着色器代码
// appdata_img定义在UnityCG.cginc
struct v2f
{
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;//维度为5的纹理数组。对应竖直采样时需要的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;//_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;
}
// 两个Pass公用片元着色器
fixed4 fragBlur(v2f i) : SV_Target
{
//这里的权值由高斯公式计算而来:
float weight[3] = {0.4026, 0.2442, 0.0545};//分别对应中心点,邻居点和邻居的邻居点的权重
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
//下面是两个pass。分别是
ZTest Always
Cull Off
ZWrite Off
Pass
{
NAME "GAUSSIAN_BLUR_VERTICAL"// 定义名字。可以从其他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"
}
5.Bloom
(1)c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class C_Bloom : PostEffectsBase
{
public Shader BloomShader;
private Material _bloomMaterial = null;
public Material material
{
get
{
_bloomMaterial = CheckShaderAndCreateMaterial(BloomShader, _bloomMaterial);
return _bloomMaterial;
}
}
//Bloom是建立在高斯模糊基础上的,所以参数基本与高斯模糊一样
[Range(0, 4)] public int iterations = 3;
[Range(0.2f, 3.0f)] public float blurSize = 0.6f;
[Range(1, 8)] public int downSample = 2;
[Range(0.0f, 2.0f)] public float luminanceThreshold = 0.6f;
//亮度阈值,控制提取较亮区域时使用的阈值大小.一般情况下图像的亮度值不会超过1,
//但是如果开启了HDR,硬件会允许颜色值被存储在一个更高精度范围的缓冲中,此时可能会超过1,所以范围定在0-4
[Range(0.1f, 2f)] public float bloomStrength = 1;//bloom强度值
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
//material.SetFloat("_BlurSize", blurSize);
material.SetFloat("_BloomStength", bloomStrength);
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtW = src.width / downSample;
int rtH = src.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear; //将该临时渲染纹理( buffer0)的滤波模式设置为双线性,使降采样有效
Graphics.Blit(src, buffer0,material,0);//进行shader第一个pass处理后的图像存于buffer0。下面就要着手对其高斯
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSize);//设置shader里的参数blurSize
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//分配第二块与屏幕大小相同的缓冲区(临时渲染纹理),以达到高斯模糊反复迭代的目的。
Graphics.Blit(buffer0, buffer1, material, 1);//第一次高斯模糊,使用shader里第一个pass(竖直方向高斯核进行滤波)
RenderTexture.ReleaseTemporary(buffer0);//释放buffer0空间,因为操作后结果已经存储到buffer1了
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);//相当于刷新buffer1
Graphics.Blit(buffer0, buffer1, material, 2);//第二次高斯模糊,使用shader里第二个pass(水平方向高斯核进行滤波)
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
material.SetTexture("_Bloom", buffer0);//将进行完亮度提取与高斯模糊后的图像,传递给shader
Graphics.Blit(src,dest,material,3);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(src, dest);
}
}
}
(2)shader
//Bloom
//辉光的步骤主要如下:
//依据亮度闸值提取图像亮部
//对提取的亮部进行高斯模糊
//将模糊的亮部与原图像叠加
//依上步骤,Bloom需要四个pass(高斯模糊两个)
Shader "Unity Shaders Book/Chapter 12/Bloom"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize("Blur Size", Float) = 1.0//模糊距离
_Bloom ("Bloom (RGB)", 2D) = "black"{}//进行亮度提取与高斯模糊后的图像,从摄像机脚本那传递而来
_BloomStength("BloomStength",Float)=1
_LuminanceThreshold("Luminance Threshold", Float) = 0.5//亮度闸值
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
sampler2D _Bloom;
float _BloomStength;
float _LuminanceThreshold;
//提取亮部部分
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);//将亮度值减去阈值并截取到0-1范围
return c * val; //将该值与原像素值相乘,得到亮部区域
}
//混合部分
struct v2fBloom {
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;//使用half4同时存储_MainTex与_Bloom的坐标
};
v2fBloom vertBloom(appdata_img v) {
v2fBloom o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
//平台差异化处理
//判断是否时DirectX平台(uv从顶部开始)
#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)*_BloomStength;
}
//高斯模糊部分
//竖直方向的顶点着色器代码
// appdata_img定义在UnityCG.cginc
struct v2fGs
{
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;//维度为5的纹理数组。对应竖直采样时需要的5个纹理坐标(中心点及上下各两个)
};
v2fGs vertBlurVertical(appdata_img v)//处理垂直模糊
{
v2fGs 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;//_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;
}
v2fGs vertBlurHorizontal(appdata_img v)//处理水平模糊
{
v2fGs 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;
}
// 两个Pass公用片元着色器
fixed4 fragBlur(v2fGs i) : SV_Target
{
//这里的权值由高斯公式计算而来:
float weight[3] = {0.4026, 0.2442, 0.0545};//分别对应中心点,邻居点和邻居的邻居点的权重
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
//定义Bloom效果需要的4个Pass
ZTest Always
Cull Off
Zwrite Off
Pass//第一个pass提取亮部
{
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
//使用在高斯模糊中定义好的两个Pass
//对提取的亮部高斯模糊
Pass
{
NAME "GAUSSIAN_BLUR_VERTICAL"// 定义名字。可以从其他Shader直接来使用该Pass
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass
{
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
//高斯模糊的两个pass也可以复用之前写的
//UsePass "MyShader/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"等
//混合图像
Pass
{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
Fallback Off
}