【技术美术百人计划】图形 4.1 Bloom算法 游戏中的辉光效果实现_哔哩哔哩_bilibili
冯乐乐大佬:《unity shader入门精要》
什么是Bloom(算法)
- Bloom,也称辉光,是一种常见的屏幕效果
- 模拟摄像机的一种图像效果,让画面中较亮的区域“扩散”到周围的区域中,造成一种朦胧的效果
- 可以让物体具有真实的明亮效果
- 可以实现光晕效果
Bloom的实现原理
- 1.首先根据一个阈值提出图像中的较亮区域,并将它们存储在一张渲染纹理中
- 2.利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果
- 3.与原图混合
https://www.yuque.com/6527chen/ldyt32/yknxgb#833c2646
这里引用了下苦咖啡好喝的笔记
前置知识1:HDR和LDR
LDR(Low Dynamic Range,低动态范围)
- JPG\PNG格式图片
- RBG范围在[0,1]之间
- 用0-1表达真实世界的色彩是不够的
HDR(High Dynamic Range,高动态范围)
- HDR\EXR格式图片
- RGB范围可在[0,1]之外
如果不用HDR,只能提取亮度小于1的区域,可能路灯与水洼的亮度一样,都会产生bloom效果,但是使用HDR,就可以提取更亮的地方,让更亮的区域产生bloom效果。超过1的地方
前置知识2:高斯模糊
- 实现图像模糊的一种方式
- 利用高斯核进行卷积运算,得到模糊的图像
高斯核
高斯核:通过高斯函数定义的卷积核
- 核中心:(0,0)
- 核大小:3x3
- 标准方差σ:1.5
计算步骤:
- 将(x,y)带入公式中,计算出权重值,(权重值代表当前处理像素的影响程度,离中心越近权重越大)
- 为了保证卷积后图像不变暗,需要对高斯核进行归一化处理(每个权重除以所有权重和)
二维高斯核的特点
一维高斯核中包括了很多重复的权重(下例中0.0545,0.02442)
下例中大小为5的高斯核,实际上只需要记录三个权重值即可
归一化的作用是:为了保证卷积后的图像不会变暗
卷积
课程内容
- 是一种图像操作
- 利用“卷积核”对图像的每个像素进行一系列操作
卷积核:
- 通常是由四方形网格结构,该区域内每个放个都有一个权重值
- 当我们对图像中的像素进行卷积时:
- 会把卷积核的中心放置在该像素上
- 翻转核之后再依次计算核中每个元素和其覆盖的图像像素值的乘积并求和
- 得到的结果就是该位置的新像素值
算法实现
C#代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//继承基类(检查是否满足条件)
public class Gaussian : PostEffectsBase
{
//定义需要传给Shader的参数
public Shader GaussianShader;//指定的Shader
private Material GaussianMat = null;//创建的Material
[Range(0,4)]
public int iterations = 1; // 高斯模糊迭代次数
[Range(1,8)]
public int downSample = 2; //降采样的次数(控制渲染纹理的大小)
[Range(0.0f, 4.0f)]
public float luminanceThreshold = 0.6f; //阈值
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f; //高斯模糊范围
public Material material
{
get
{
GaussianMat = CheckShaderAndCreateMaterial(GaussianShader, GaussianMat);
//创建的Material=函数(指定的Shader,创建的Material(get函数调用基类的函数来得到对应的材质))
return GaussianMat;
}
}
//使用OnRenderImage获取当前屏幕图像,检查材质是否可用,
protected void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//null:直接让结果显示在屏幕上,不做任何处理
if (material != null) {
//向Shader传递亮度阈值参数
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
//定义长宽高
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//定义buffer0缓冲区的大小
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0);
//设置buffer0为双线性滤波
buffer0.filterMode = FilterMode.Bilinear;
//调用Shader中第一个Pass来提取图像,并存储在buffer0中
Graphics.Blit(source, buffer0, material, 0);
//for循环,iteration次数
for (int i = 0; i < iterations; i++)
{
//将高斯模糊范围传入Shdaer(等下Shader需定义),乘以范围使每次迭代模糊范围不同
material.SetFloat("_BlurSize",1.0f + blurSpread*i);
//定义buffer1,
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//对图像进行竖直方向的高斯模糊。buffer0(Scr),buffer1(dest),material(使用的材质),1(Shader中第二个pass)
Graphics.Blit(buffer0,buffer1,material,1);
//释放buffer0
RenderTexture.ReleaseTemporary(buffer0);
//重新赋值
buffer0 = buffer1;
//重新分配buffer1,准备接受第二次图像数据
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//对图像进行垂直方向的高斯模糊。
Graphics.Blit(buffer0, buffer1, material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
//material中将buffer0赋予给_Bloom
material.SetTexture("_Bloom",buffer0);
//渲染图像到dest上
Graphics.Blit(source,destination,material,3);
//释放buffer0
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(source,destination);//材质为空直接输入原图
}
}
}
Shader
Shader "Unlit/Guassian"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}//Src原图
_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;
//定义v2f
///pos
///uv
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
//提取亮度的顶点着色器,appdata_img
///顶点转换,uv赋值
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;
}
//提取亮度的片元着色器
///先进行纹理采样
///进行亮度提取,采样得到的亮度值(_LuminanceThreshold)减去阙值,并截取到0-1中
///得到的值和原像素值相乘,得到提取后的亮部区域
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 v2fGaussian {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;//一维高斯核的UV
};
v2fGaussian vertBlurVertical(appdata_img v) {
v2fGaussian 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;
}
v2fGaussian vertBlurHorizontal(appdata_img v) {
v2fGaussian 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(v2fGaussian i) : SV_Target {
//pass1 pass2片元着色器
//卷积计算
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);
}
///--------混合亮部图像和原图像-----------
///pos和uv,这里的uv是四维xy:_MainTex zw:_Bloom
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; //zw是_Bloom,即模糊后的较亮区域的纹理坐标
//平台差异化处理
#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 ,剔除,深度写入
ZTest Always Cull Off ZWrite Off
Pass { //去除较亮的图像
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
//use pass 的名字会转换成大写字母,使用时要用大写
//usepass in GAUSSIAN_BLUR_VERTICAL
//usepass in GAUSSIAN_BLUR_HORIZONTAL
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
}
Pass {
CGPROGRAM //合并原始图像和模糊的亮部图像
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}
Bloom算法的应用
配合自发光贴图使用
配合特效
GodRay效果(基于径向模糊的后处理)
模拟光线往某个方向扩散的效果
配合Tonemapping(色调映射)
从HDR到LDR的过程叫做tone mapping
可以保留暗处和亮处的细节
GoodRay效果
使用径向模糊代替高斯模糊,模拟光线往某个方向扩散的效果
GoodRay配合ToneMapping
可以看到ToneMapping解决了过曝的问题
作业