【Unity Shader】屏幕后处理4.0:基于高斯模糊的Bloom

news2024/11/26 12:29:50

原本打算写高斯模糊和双重模糊两个实现Bloom方法的对比,但两个加在一起篇幅过长,于是拆成两篇文章来进行。

学习前建议应先搞清楚的几个概念

  • HDR
  • LDR
  • ToneMapping
  • 几种模糊算法

1 高斯模糊实现Bloom

最近一直在学习Unity Shader实现各种后处理效果,Bloom效果就是其中之一,它也是游戏中最常见的效果之一,也是必不可少的效果之一吧!百人计划就有专门介绍实现Bloom的过程,跟《Unity Shader 入门精要》12.5章介绍的Bloom效果实现方法是一样的:

  • 根据一个阈值提取屏幕图像中较亮的区域,储存在一张RT中
  • 利用高斯模糊进行模糊处理
  • 将模糊后的结果与最初的屏幕图像混合

经过以上三个大步骤就得到了最终的Bloom效果,步骤的Shader一共包含了4个Pass(中间2个Pass是高斯模糊分成的水平和竖直方向)。先不谈这个方法跟其他方法比较有什么优劣,我们先过一遍实现过程。

1.1 C#脚本

跟之前写过的边缘检测/高斯模糊后处理一样,实现Bloom也需要C#脚本和Unity Shader一起完成。

完整脚本在这:

//jiujiu345
//2022.11.14
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine;

[ExecuteInEditMode]
public class Bloom : MonoBehaviour
{
    public Shader bloomShader;
    public Material bloomMaterial;
    [Header("模糊迭代次数")]
    [Range(0, 4)]
    public int interations = 2;

    [Header("模糊范围")]
    [Range(0.3f, 3.0f)]
    public float blurSpread = 0.3f;

    [Header("降采样次数")]
    [Range(1, 8)]
    public int downSample = 1;

    //控制提取较亮区域时使用的阈值大小
    //开启HDR后,亮度值会超过1,所以范围在1~4
    [Header("阈值")]
    [Range(0.0f, 4.0f)]
    public float luminanceThreshold = 0.6f;
    public void OnRenderImage (RenderTexture source, RenderTexture destination)
    {
        if(bloomMaterial != null)
        {
            bloomMaterial.SetFloat("_LuminanceThreshold", luminanceThreshold);

            int rtW = source.width / downSample;
            int rtH = source.height / downSample;
            //定义rt
            RenderTexture rt0 = RenderTexture.GetTemporary(rtW, rtH, 0);
            rt0.filterMode = FilterMode.Bilinear;
            //第一个Pass提取图片较亮的部分
            Graphics.Blit(source, rt0, bloomMaterial, 0);

            for(int i = 0; i < interations; i++)
            {
                bloomMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread);

                RenderTexture rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                //第二个Pass
                Graphics.Blit(rt0, rt1, bloomMaterial, 1);
                RenderTexture.ReleaseTemporary(rt0);
                rt0 = rt1;
                rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                //第三个Pass
                Graphics.Blit(rt0, rt1, bloomMaterial, 2);
                RenderTexture.ReleaseTemporary(rt0);
                rt0 = rt1;
            }
            //rt0储存模糊后的图片
            bloomMaterial.SetTexture("_Bloom", rt0);
            //第四个Pass-混合
            Graphics.Blit(source, destination, bloomMaterial, 3);
            RenderTexture.ReleaseTemporary(rt0);
        }
        else
        {
            Debug.Log("Please input your Material");
            Graphics.Blit(source, destination);
        }
    }
}

可控参数

脚本中提供了四个用户可控参数:

  • 模糊迭代次数——模糊效果不断叠加
  • 模糊范围——每次高斯模糊的模糊范围
  • 降采样次数
  • 阈值——亮度超过该阈值的区域才能被提取

传递给Material的参数

通过.SetFloat()进行了基本参数传递,分别传递了:

  • luminanceThreshold——阈值
  • blurspread——模糊范围,_BlurSize=1+i*blurSpread,迭代次数越多范围越大

通过.SetTexture()传递了储存模糊结果的渲染纹理_Bloom

四个Pass

四个Pass都是通过调用Graphics.Blit()实现的,高斯模糊的第二和第三个Pass由于要实现模糊迭代,引入了一个for循环进行模糊迭代,两个渲染纹理rt0rt1交替储存结果:

for(int i = 0; i < interations; i++)
{
    bloomMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread);

    RenderTexture rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);

    //第二个Pass
    Graphics.Blit(rt0, rt1, bloomMaterial, 1);
    RenderTexture.ReleaseTemporary(rt0);
    rt0 = rt1;
    rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);

    //第三个Pass
    Graphics.Blit(rt0, rt1, bloomMaterial, 2);
    RenderTexture.ReleaseTemporary(rt0);
    rt0 = rt1;
}

1.2 Shader代码

即四个Pass的具体顶点和片元着色器,完整代码在这:

//jiujiu345
//2022.11.14
Shader "Unity Shaders Book/Chapter 12/Bloom_GaussianBlur"
{
    Properties
    {
        _MainTex ("Base(RGB)", 2D) = "white" {} //src
        _Bloom("Bloom(RGB)", 2D) = "black" {} //高斯模糊后的较亮区域
        //无须定义在Shader面板,采取C#脚本控制
        //_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;

        //Pass0-提取较亮区域
        struct v2f_Extract {
            float4 pos : SV_POSITION;
            half2 uv : TEXCOORD0; 
        };

        v2f_Extract vertExtractBright(appdata_img v) {
            v2f_Extract 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控制亮度强度
        fixed4 fragExtractBright(v2f_Extract i) : SV_Target {
            fixed4 c = tex2D(_MainTex, i.uv);
            fixed val = saturate(Luminance(c) - _LuminanceThreshold);
            return val * c;
        }

        //Pass2&3-高斯模糊
        struct v2f_Gaussian {
            float4 pos : SV_POSITION;
            half2 uv[5] : TEXCOORD0;  
        };

        //水平
        v2f_Gaussian vertBlurVertical(appdata_img v) {
            v2f_Gaussian o;
            o.pos = UnityObjectToClipPos(v.vertex);
            half2 uv = v.texcoord;

            o.uv[0] = uv;
            o.uv[1] = uv + float2(0.0f, _MainTex_TexelSize.y * 1.0 * _BlurSize);
            o.uv[2] = uv - float2(0.0f, _MainTex_TexelSize.y * 1.0 * _BlurSize);
            o.uv[3] = uv + float2(0.0f, _MainTex_TexelSize.y * 2.0 * _BlurSize);
            o.uv[4] = uv - float2(0.0f, _MainTex_TexelSize.y * 2.0 * _BlurSize);

            return o;
        }

        //竖直
        v2f_Gaussian vertBlurHorizontal(appdata_img v) {
            v2f_Gaussian o;
            o.pos = UnityObjectToClipPos(v.vertex);
            half2 uv = v.texcoord;

            o.uv[0] = uv;
            o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0 * _BlurSize, 0.0f);
            o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0 * _BlurSize, 0.0f);
            o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0 * _BlurSize, 0.0f);
            o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0 * _BlurSize, 0.0f);

            return o;
        }

        fixed4 GaussianBlur(v2f_Gaussian i) : SV_Target {
            float weight[3] = {0.4026, 0.2442, 0.0545};
            fixed3 color = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
            
            for(int j=1;j<3;j++) {
                color += tex2D(_MainTex, i.uv[2*j-1]).rgb * weight[j];
                color += tex2D(_MainTex, i.uv[2*j]).rgb * weight[j];
            }
            return fixed4(color, 1.0);
        }

        //Pass3-混合亮度和原图
        struct v2f_Bloom {
            float4 pos : SV_POSITION;
            half4 uv : TEXCOORD0;
        };

        v2f_Bloom vertBloom(appdata_img v) {
            v2f_Bloom o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv.xy = v.texcoord;
            o.uv.zw = v.texcoord;

            //用以判断是否在Direct3D平台
            #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(v2f_Bloom 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 GaussianBlur

            ENDCG
        }

        Pass {
            CGPROGRAM
            #pragma vertex vertBlurHorizontal
            #pragma fragment GaussianBlur

            ENDCG
        }

        Pass {
            CGPROGRAM
            #pragma vertex vertBloom
            #pragma fragment fragBloom

            ENDCG
        }
    }
    FallBack  Off
}

关于Properties语义块声明的属性

发现在Properties语义只声明了两个用到的Texture属性:

  • 一个是source纹理会被传递给Shader中的_MainTex
  • 一个是C#脚本中定义的储存模糊后图像的渲染纹理_Bloom

其实这里_Bloom也是可以省略的,但是加上的话就可以在材质面板上实时看到模糊后的图像了。接下来可以直接在CGINCLUDE--ENDCG定义的代码段中定义Shader需要的变量,很大一部分直接来自C#脚本传入的变量。

还有一点废话:如果一个变量在Properties中被声明,又在C#脚本中被定义,可能会出现脚本界面改动变量无效的情况,为了避免这个情况我们只其一就行(我一般选择脚本界面改动)。

如何提取亮度区域?

根据RGB三通道的值转亮度值,具体为什么这么做可以参考这篇文章的亮度部分:

//计算像素的亮度
        fixed Luminance(fixed4 color) {
            return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
        }

 接着加入C#脚本传递过来的阈值参数,用一个saturate(Luminance(c) - _LuminanceThreshold)提取阈值以上的亮度。

关于高斯模糊那块儿,上一篇博客已经涉及到了,这里直接搬过来代码就行!

1.3 Bloom效果展示

参数设置如下:

后处理前后对比如下:

1.4 FrameDebug看看过程

又要打开我们的老朋友Frame Debugger了:

可以看到整个后处理经历了6个步骤,如下动图:

经历的Pass顺序是:Pass0 -> Pass1 -> Pass2 -> Pass1 -> Pass2 -> Pass3,经历两边1&2是因为迭代次数选择了2.

2 进一步实现自发光Bloom

Unity其实是自带Bloom效果的,我们直接上对比就能感受到区别了。

2.1 准备场景

场景中拖入随便一个模型,关闭场景中的光源(为了更好的观察效果),然后给模型拖入Unity的Standard材质,打开Emission,设置如下:

未进行任何后处理的初始效果如下:

接下来进行对比操作。

2.2 方法1:高斯模糊

基于当前的代码实现自发光Bloom,效果其实是比较差的。。.下图是我尝试调整到了最能体现自发光的效果:

2.3 方法2:Unity自带的Bloom

顺便提一嘴:Unity自带的Bloom应该是采用降采样+升采样——双重模糊的模糊算法(我不是特别确定),关于到底如何降/升采样我们后面就会讲到。

我们再用Unity自带的Post Processing里的Bloom效果 (添加方式可参考(50) EPIC GLOW IN UNITY 2020.2 - YouTube),看看出来的效果是什么样的:

由于参数设置等一些原因没办法从性能上进行一个对比,但单从两种方法“能调整到的最好的实现效果”上,后者完胜。

3 谈谈高斯模糊实现Bloom的优缺点

推荐结合这一篇文章来看第3小节。

3.1 优点

不知道算不算“优点”:这个方法对于实现简单的大片的泛光加强本来亮点就很明显的场景泛光效果还是蛮不错的,除了上述天空的例子,再举两个例子:

(原图来自ArtStation - Jinji's Grotto, Connor Sheehan)

 (原图来自ArtStation - Vermillion Forest, Anton Fadeev)

3.2 缺点1:性能上

因为模糊算法用的是高斯模糊,高斯模糊本质还是卷积核,如果我们想要大范围的Bloom效果,就只能靠增大滤波范围or增加滤波次数来实现,基于上述代码的话采取操作分别是:

  • 增加滤波次数——Interation↑
  • 增大滤波范围——BlurSpread↑

首先增大滤波次数相当于多来几次Pass1&2,一下子性能消耗就上去了,其次如果真的实践你会发现,无论是增加滤波次数还是增大滤波范围,达到的扩大效果也不是很能让人满意。

3.3 缺点2:实现效果上

除了上述的自发光实现效果,我在调整的过程中还感受到:高斯模糊实现的自发光Bloom总是有一种明显的边界感。这是为什么?

我猜应该是因为高斯模糊(感觉只要是基于卷积核的模糊都是一样的)总是依据卷积核每一格的权重进行加权平均计算出中间项的值,所以即使降采样了,每一level之间还是会存在明显的亮度突变,于是源图和模糊后的图的亮度无论调整哪个参数都不会过渡均匀。


下一篇将会介绍双重模糊实现Bloom的方法。(断更了很久了,最近简直是从石头缝里挤时间来额外学习TA的内容,太难了太难了)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/27523.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++ - 第13节 - c++中的继承

1.继承的概念及定义 面向对象三大特性&#xff1a;封装、继承、多态注&#xff1a;面向对象不止这三个特性&#xff0c;还有其他特性&#xff0c;比如反射&#xff08;Java中的概念&#xff09;、抽象等封装的理解&#xff1a;&#xff08;1&#xff09;将c设计的stack类&#…

记一次艰难的上班历程

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 以下事件均为卢松松真实经历&#xff1a; 早上7点&#xff0c;小区又被静默了&#xff0c;几百号人堵在小区门口。 我不顾病毒传染的风险挤到了小区门口&#xff0c;问原因。 看门的说到&#x…

pumping lemma

正规语言版本 LLL是正规语言,则存在整数p≥1p\ge 1p≥1 对于任意长度大于等于ppp的字符串w∈Lw\in Lw∈L&#xff0c;wxyzwxyzwxyz,满足下面3个条件 ∣y∣≥1\left|y\right|\ge 1∣y∣≥1 ∣xy∣≤p\left|xy\right|\le p∣xy∣≤p ∀n≥0,xynz∈L\forall n\ge 0,xy^nz\in L∀n≥…

1997-2020年各省三废排放量和熵值法计算的环境规制综合指数(无缺失值)

1997-2020年各省三废排放量和环境规制综合指数 1、包括&#xff1a;30个省份 2、指标包括&#xff1a;工业二氧化硫排放量、工业烟尘排放量和工业废水排放量 环境规制综合指数是由工业废水排放量、工业 SO2 排放量以及工业烟尘排放量计算而来 &#xff08;表格中有详细的三…

网络流量监测与调度技术研究

网络流量监测与调度技术研究网络流量监测与调度技术研究学习目标&#xff1a;流量监测学习内容&#xff1a;流量监测流量监测的设计框架框架一框架二框架三申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计…

零基础上手unity VR开发【Oculus账号体系准备】

&#x1f4cc;个人主页&#xff1a;个人主页 ​&#x1f9c0; 推荐专栏&#xff1a;Unity VR 开发成神之路 --【着重解决VR项目开发难&#xff0c;国内资料匮乏的问题。从零入门&#xff0c;一步一个脚印&#xff0c;带你搞定VR开发! &#x1f63b;&#x1f63b;】 &#x1f4d…

大数的乘法

输入一个大正整数和一个非负整数&#xff0c;求它们的积。 输入格式: 测试数据有多组&#xff0c;处理到文件尾。每组测试输入1个大正整数A&#xff08;位数不会超过1000&#xff09;和一个非负整数B&#xff08;int范围&#xff09;。 输出格式: 对于每组测试&#xff0c;…

[Leetcode刷题] - LC003 Longest Substring without repating character

题目链接 Leetcode 003Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.https://leetcode.com/problems/longest-substring-without-repeating-characters/ 题目描述 给定…

数字先锋 | 随时随地云端阅片,“云胶片”时代来啦!

作为现代医疗必不可少的诊断方法&#xff0c;医学影像数据在医疗数据中的占比高达90%且正以每年30%的速度递增&#xff0c;而影像医生就业人数年增长率仅4%。这意味着&#xff0c;全国总人数不到20万的放射科医生&#xff0c;要处理每年75.4亿人次影像相关诊断需求&#xff0c;…

代理模式-P19

静态代理&#xff1a; 创建项目&#xff1a; Rent package com.Li.demo01;//租房 public interface Rent {public void rent(); }Proxy&#xff1a; package com.Li.demo01; //中介&#xff08;负责找房东&#xff09; public class Proxy implements Rent{//private Host h…

Redis安装及使用(WindowsLinux)

Windows 1.下载 下载地址&#xff1a;Releases tporadowski/redis GitHub。 目前最新5.0.14 2.解压 3.先后打开redis-server.exe和redis-cli.exe两个文件&#xff08;一定要按顺序&#xff09; 4.使用 正常使用在redis-cli.exe里面输入各种命令&#xff08;使用期间redis…

7、系统管理

文章目录7、系统管理7.1 Linux 中的进程和服务7.2 service 服务管理&#xff08;CentOS 6 版本-了解&#xff09;7.2.1 基本语法7.2.2 经验技巧7.2.3 案例实操&#xff08;1&#xff09;查看网络服务的状态&#xff08;2&#xff09;停止网络服务&#xff08;3&#xff09;启动…

基于JAVA的个人博客论坛系统的设计与实现参考【数据库设计、源码、开题报告】

在学校开发搭建一个什么项目最有成就感&#xff0c;那肯定就是搭建「个人博客」呀&#xff0c;然后把自己平时的学习笔记写到博客里&#xff0c;这时你的笔记就是**云笔记**&#xff0c;就再也不会出现因为本地文件丢失而感到痛心的事情。 而且&#xff0c;还可以把你的个人博客…

艾美捷小鼠肿瘤坏死因子α-ELISpot试剂盒使用说明

肿瘤坏死因子-α 肿瘤坏死因子-α&#xff08;TNF-α&#xff0c;也称为TNF-α、TNF-a、TNF-a和肿瘤坏死因子α&#xff09;由许多不同的细胞类型产生&#xff0c;例如单核细胞、巨噬细胞、T细胞和B细胞。TNF-α的许多作用包括保护细菌感染、细胞生长调节、免疫系统调节和感染…

Kotlin 使用vararg可变参数

文章目录背景Kotlin中使用可变参数对Kotlin可变参数反编译资料背景 一般在项目开发中&#xff0c;我们经常会在关键节点上埋点&#xff0c;而且埋点中会增加一些额外参数&#xff0c;这些参数通常是成对出现且参数个数是不固定的。如下&#xff1a; //定义事件EVENT_ID const…

云原生|kubernetes |使用Prometheus监控k8s cAdvisor篇(一)(centos)

前言: 为了能够提前发现kubernetes集群的问题以及方便快捷的查询容器的各类参数&#xff0c;比如&#xff0c;某个pod的内存使用异常高企 等等这样的异常状态&#xff08;虽然kubernetes有自动重启或者驱逐等等保护措施&#xff0c;但万一没有配置或者失效了呢&#xff09;&am…

PAM exec模块监控服务器ECS登录用户及IP,触发登录后,发送至钉钉

PAM 模块pam_exec &#xff0c;用于运行外部命令 PAM 项作为环境变量导出&#xff1a;* PAM_RHOST &#xff0c; PAM_RUSER &#xff0c; PAM_SERVICE &#xff0c; PAM_TTY &#xff0c; PAM_USER 和 PAM_TYPE *&#xff0c;其中包含以下模块类型之一&#xff1a;account&…

【深度学习】实验5布置:滴滴出行-交通场景目标检测

DL_class 学堂在线《深度学习》实验课代码报告&#xff08;其中实验1和实验6有配套PPT&#xff09;&#xff0c;授课老师为胡晓林老师。课程链接&#xff1a;https://www.xuetangx.com/training/DP080910033751/619488?channeli.area.manual_search。 持续更新中。 所有代码…

电脑c盘分区太小如何可以扩大,电脑c盘不够用了,如何给电脑分区

C盘是很重要的系统盘&#xff0c;会影响到整个系统的正常化运行。如果电脑c盘分区太小如何可以扩大&#xff1f; 一、清理C盘的垃圾文件 C盘分区不足的原因之一就是因为C盘的垃圾文件没有及时清理&#xff0c;导致垃圾文件越来越多&#xff0c;电脑越来越卡&#xff0c;所以&a…

2022最新软件测试面试八股文,全网最全最新,堪称地表最强

前言 2022秋招已经结束&#xff0c;在应聘过程中相信很多同学都感受到了目前测试就业越来越严峻&#xff0c;内卷也成了测试领域的代名词了。的一个HR朋友告诉我&#xff0c;由于门槛较低&#xff0c;现在普通测试岗&#xff08;偏功能&#xff09;的投递比已经将近100&#x…