【TA-霜狼_may-《百人计划》】图形4.1 Bloom算法 游戏中的辉光效果实现
【TA-霜狼_may-《百人计划》】图形4.1 Bloom算法 游戏中的辉光效果实现
4.1.1 Bloom算法介绍
模拟摄像机的一种图像效果;
实现思路
前置知识
HDR与LDR
HDR使得亮度提取步骤更加方便准确。
高斯模糊
利用高斯核对图像进行卷积。
高斯核计算方法:
计算量:可以假想图像中的任意一个像素点,它将会出现在高斯核中上的每个位置,也就是NN,而这样的点总共有WH(图像的长宽)个,所以总共需要的计算是NNWH;
如果将二维高斯核替换为两个一维,则上面步骤中的NN变为2*N,当高斯核较大时可以起到很好的降低运算量的效果。
并且一维高斯核存在对称性,即在存储的时候可以只存储一半的权值。
4.1.2 Bloom效果实现
用于相机的csharp脚本,
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyBloom : PostEffectsBase
{
// 首先定义使用的shader和材质
public Shader bloomShader;
public Material bloomMaterial = null;
public Material material{
get{
// 调用PostEffectsBase基类中的函数,检查shader并且创建材质
bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
return bloomMaterial;
}
}
// 定义shader中的参数
// 高斯模糊迭代次数
[Range(0,4)] public int iterations = 3;
// 高斯模糊范围
[Range(0.2f, 3.0f)] public float blurSpread = 0.6f;
// 下采样,缩放系数
[Range(1, 8)] int downSample = 2;
// 高亮提取阈值
[Range(0.0f, 4.0f)] public float luminanceThreshold = 0.6f;
// 调用OnRenderImage函数来实现Bloom
private void OnRenderImage(RenderTexture src, RenderTexture dest) {
if(material != null){
// 传入阈值
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
// src.width 和 hight 代表屏幕图像的宽度和高度
int rtW = src.width / downSample;
int rtH = src.height / downSample;
// 创建一块分辨率小于原屏幕的缓冲区:buffer0
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0);
buffer0.filterMode = FilterMode.Bilinear;
// 用Blit方法调用shader中的第一个pass,提取图像中较亮的区域
// 结果存在buffer0中
Graphics.Blit(src, buffer0, material, 0);
// 迭代进行高斯模糊
for (int i = 0; i < iterations; i++){
// 传入模糊半径
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
// 定义第二个缓冲区:buffer1
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 用Blit方法调用shader中的第二个pass,进行竖直方向上的高斯模糊
// 结果存在buffer1中
Graphics.Blit(buffer0, buffer1, material, 1);
// 释放缓冲区buffer0,将buffer1中的数值赋值给buffer0,并重新分配buffer1
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 用Blit方法调用shader中的第三个pass,进行水平方向上的高斯模糊
// 结果存在buffer1中
Graphics.Blit(buffer0, buffer1, material, 2);
// 再次调换顺序
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
// 最终高斯模糊的结果存储在buffer0中
}
// 将完成高斯模糊的结果buffer0传递给材质中的_Bloom微粒属性
material.SetTexture("_Bloom", buffer0);
// 用Blit方法调用shader中的第四个pass,完成混合
// dest为最终输出
Graphics.Blit(src, dest, material, 3);
// 最后别忘记释放临时缓冲区
RenderTexture.ReleaseTemporary(buffer0);
}
else{
Graphics.Blit(src, dest);
}
}
}
对调用Pass那一部分的代码进行了修改,没有必要将数据在buffer0,和buffer1中倒来倒去,徒增功耗,修改如下,测试后效果不变(经过进一步测试发现,重新为buffer0那一步赋值也是没有必要的,直接两句话就可以搞定了)(再更新,buffer1要手动释放,不然每次都重新申请就会爆内存!!):
// 结果存在buffer1中
Graphics.Blit(buffer0, buffer1, material, 1);
// 将buffer0作为接收用的缓存
// buffer0 = RenderTexture.GetTemporary(rtW,rtH,0);
// 利用buffer1中的数据进行水平方向上的高斯模糊,结果保存在buffer0中
Graphics.Blit(buffer1, buffer0, material, 2);
// 替换以下代码
// // 释放缓冲区buffer0,将buffer1中的数值赋值给buffer0,并重新分配buffer1
// RenderTexture.ReleaseTemporary(buffer0);
// buffer0 = buffer1;
// buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// // 用Blit方法调用shader中的第三个pass,进行水平方向上的高斯模糊
// // 结果存在buffer1中
// Graphics.Blit(buffer0, buffer1, material, 2);
// // 再次调换顺序
// RenderTexture.ReleaseTemporary(buffer0);
// buffer0 = buffer1;
// // 最终高斯模糊的结果存储在buffer0中
bloom shader:
Shader "Custom/MyBloomShader"
{
Properties
{
_MainTex ("Texture", 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;
// 用于第一个pass
// 输出结构体
struct v2fExtractBright{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
// 顶点着色器
v2fExtractBright vertExtractBright(appdata_img v){
v2fExtractBright 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(v2fExtractBright i): SV_TARGET{
// 采样贴图
fixed4 c = tex2D(_MainTex, i.uv);
// 利用clamp函数将亮度值截取在0,1范围内
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
// 将val与原图采样得到像素值相乘,得到提取后的亮部区域
return c * val;
}
// 第二、三个pass
struct v2fBlur{
float4 pos: SV_POSITION;
half2 uv[5]: TEXCOORD0;
// 此处定义大小为5的数组来存储5个纹理坐标
// 用于卷积核大小为5x5的二维高斯核可以拆分为两个大小为5的1维高斯核
// uv[0]存储了当前的采样纹理
// uv[1-4]为高斯模糊中对邻域采样时使用的纹理坐标
};
// 在顶点着色器中计算竖直方向进行高斯模糊的uv坐标
v2fBlur vertBlurVertical(appdata_img v){
v2fBlur 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;
}
// 在顶点着色器中计算竖直方向进行高斯模糊的uv坐标
v2fBlur vertBlurHorizontal(appdata_img v){
v2fBlur 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(v2fBlur i): SV_TARGET{
// 利用二维高斯核的可分离性,分离得到具有对称性的一维高斯核
// 因此只需要在数组中存放三个高斯权重即可
float weight[3]= {0.4026, 0.2442, 0.0545};
// 0,0位置的值直接乘上当前位置的权重保存下来
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使用
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
}
Pass{
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass{
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
Pass{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}
4.1.3 Bloom效果应用
部分区域自发光,烟花,阳光,GodRay
GodRay仔细看可以看到光柱的感觉,(基于径向模糊的后处理)
结合Tonemapping实现更好的光线效果:
作业
原图未使用Bloom效果:
开启bloom效果:
这里注意要手动释放buffer1,教程里好像只释放了buffer0,当在测试时会发现buffer在疯狂增长,最后爆内存。
有关局部bloom:
想要利用Alpha进行调控局部bloom,但是一开始发现始终读不到屏幕的alpha值,debug了半天才想到,在物体shader的片元着色器中把A通道的值默认设置为1了,所以屏幕后处理的过程中alpha值始终为1,无法起到alpha蒙版的作用。
对原有的shader进行修改,达到了部分区域bloom的效果。
下图右侧有一条分割线,右半部分没有bloom,左半部分有bloom:
其中透明蒙版利用ps进行制作,如下:
代码部分对pass3的fragBloom进行修改,并添加判断函数:
fixed4 bloomMask0(fixed4 src, fixed4 bloomColor){
// 透明区域src.a为0
if(1 > 1-src.a){
return bloomColor;
}else{
return src;
}
}
fixed4 fragBloom(v2fBloom i): SV_TARGET{
fixed4 originalImg = tex2D(_MainTex, i.uv.xy);
fixed4 bloomResult = tex2D(_MainTex,i.uv.xy)+ tex2D(_Bloom, i.uv.zw);
return bloomMask0(originalImg, bloomResult);
}
作业部分的内容主要照着这位大佬做的,通透。