边缘检测:https://www.yukicat.net/1390/

高斯模糊:https://www.yukicat.net/1458/

暂时先记录一下如何实现Bloom效果,由于参加了一个计划,关于Bloom效果会补充更多知识,除了书上已有的知识点以外,后续会补充一些实际应用的举例,先来简单介绍Bloom效果的原理与基本实现吧。

Bloom效果,也称辉光效果或泛光效果,它是一种常见的屏幕效果,它可以模拟真实摄像机的一种图像效果,就是把画面中较亮的区域通过阈值扩散到周围的区域中,从而造成一种朦胧感。

而Bloom的实现思路是:通过确定阈值来提取图像中较亮的区域,把这个区域存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后将其与原图像进行混合即可。

那Bloom效果会出现在什么地方呢?大家在夜晚散步查看四周夜景的时候往往很容易发现,有些广告牌、装饰的灯光,还有路灯,在亮度特别亮的时候都会产生这种效果,就像下图一样:

马路旁的路灯、远处建筑的Logo灯光装饰等,都会有光线扩散到附近的一种朦胧效果,即Bloom效果

之所以产生了Bloom效果,是因为还与HDR有关。HDR(High Dynamic Range)指的是高动态范围,与之相对的是LDR(Low Dynamic Range,低动态范围),先来介绍一下LDR吧:一般JPG、PNG格式的图片均为低动态范围图像,它们都是用8位或16位整数数据存储颜色的,虽然色彩数高达256³(16777216),但它们的RGB通道对应的亮度级别只有2⁸(256),远远无法达到真实世界所需的范围。

而HDR使用了远远高于8位的精度来记录亮度信息,来表达亮度超过[0, 1]的亮度值,不会丢失高亮度区域的颜色值,从而可以更加精确的反应真实世界的光照环境,虽然最后仍然需要将HDR转换到LDR设备下进行显示,但转换的过程仍然可以最大限度地保留需要的亮度细节。但HDR由于使用了浮点数来存储高精度图像,需要更多显存空间,导致渲染速度变慢,一些硬件也并不支持HDR,所以要根据实际情况选择是否开启HDR。

所以,Bloom效果需要开启HDR,通过检测图像中某个像素的亮度值是否大于阈值,来确定提取较亮区域的范围,并进行一定程度的模糊,最后叠加在原图像上。要实现上述过程,就必须使用HDR,否则就只能使用小于1作为阈值来提取像素,但有些区域可能会接近白色从而导致提取了错误的区域,而且一些非常亮的地方的亮度值也是会大于1的,所以使用HDR就可以对亮度值超过1的像素进行提取即可实现Bloom效果。

在Unity中,实现Bloom效果,需要进行以下的准备工作:

1、新建场景,将天空盒去除; 
2、往项目中导入一张需要实现Bloom效果的图片,这里我选用一张曾经拍摄过的天空;
3、新建脚本,将脚本绑定在摄像机上; 
4、新建Shader; 
5、将导入的图片类型设置为Sprite,把图片拖入到场景里头,调整大小和位置。
实现Bloom效果的样图

在新建的Bloom脚本中,继承PostEffectsBase基类,这个基类的代码在最开始的文章有贴过完整的代码,这里就不重复记录了。老规矩,声明Bloom效果需要的Shader,并创建相应的材质。

    // 声明Bloom效果需要的Shader,并创建相应的材质
    public Shader bloomShader;
    private Material bloomMaterial = null;
    public Material material
    {
        get
        {
            // 调用PostEffectsBase基类中检查Shader和创建材质的函数
            bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
            return bloomMaterial;
        }
    }

因为Bloom效果需要利用到高斯模糊,所以在上一篇文章的脚本与高斯模糊相关变量的基础上,再增加一个luminanceThreshold变量,该变量为亮度阈值。

    // 因为Bloom效果是建立在高斯模糊的基础上完成的
    // 所以增加luminanceThreshold变量来控制并提取图像较亮区域时所使用的阈值
    // 高斯模糊迭代次数,次数越多越模糊
    [Range(0, 4)]
    public int iterations = 3;
    // 高斯模糊范围,范围越大越模糊
    [Range(0.2f, 3.0f)]
    public float blurSpread = 0.6f;
    // 缩放系数
    [Range(1, 8)]
    public int downSample = 2;
    // 阈值,与HDR有关
    [Range(0.0f, 4.0f)]
    public float luminanceThreshold = 0.6f;

接着调用OnRenderImage函数实现屏幕后处理,绝大部分的代码与实现高斯模糊时所使用的代码基本相同,思路、代码、注释如下:(对应书本第260页)

// 调用OnRenderImage函数实现Bloom效果
    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        // 先检查材质是否可用,如果可用,则将参数传递给材质后
        // 再调用Graphics.Blit进行处理,否则不作处理
        if (material != null)
        {
            // 先将阈值传给材质
            material.SetFloat("_LuminanceThreshold", luminanceThreshold);
            // src.width和src.height分别为屏幕图像的宽度与高度
            // 除以下采样得到的rtW和rtH分别为渲染纹理的宽度和高度
            int rtW = src.width / downSample;
            int rtH = src.height / downSample;

            // 创建一块大小小于原屏幕分辨率的缓冲区buffer0
            // 设置该临时渲染纹理的滤波模式为双线性
            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
            buffer0.filterMode = FilterMode.Bilinear;
            // 调用Shader中第一个Pass来提取图像中较量的区域
            // 结果存储在buffer0并输出
            Graphics.Blit(src, buffer0, material, 0);

            // 通过循环迭代高斯模糊
            for (int i = 0; i < iterations; i++)
            {
                // 将模糊半径传入Shader
                material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
                // 定义第二个缓存buffer1
                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
                // 通过Blit调用Shader中第二个Pass完成竖直方向上的高斯模糊
                Graphics.Blit(buffer0, buffer1, material, 1);
                // 接着释放buffer0,把结果重新赋给buffer0,并重新分配一次buffer1
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
                buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                // 执行第三个Pass,完成水平方向上的高斯模糊
                Graphics.Blit(buffer0, buffer1, material, 2);
                // 接着释放buffer0,把结果重新赋给buffer0
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
            }

            // 将完成高斯模糊后的结果buffer0传递给材质中的_Bloom纹理属性
            // 再次调用Graphics.Blit使用第四个Pass完成混合,dest作为最终结果输出
            // 最后释放临时缓存
            material.SetTexture("_Bloom", buffer0);
            Graphics.Blit(src, dest, material, 3);
            RenderTexture.ReleaseTemporary(buffer0);
        } else {
            Graphics.Blit(src, dest);
        }
    }

上述代码块调用了多次Graphics.Blit()函数,即通过四个Pass实现Bloom效果,后续会继续说明每个Pass的具体作用是什么,到此脚本部分的代码已完成,接下来在Shader中定义需要使用到的属性变量:

    Properties
    {
		// _MainTex为渲染纹理,变量名固定不能改变
		// 其他三个属性分别为高斯模糊后较亮的区域、阈值、模糊半径
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_Bloom ("Bloom (RGB)", 2D) = "black" {}
		_LuminanceThreshold ("Luminance Threshold", Float) = 0.5
		_BlurSize ("Blur Size", Float) = 1.0
    }

与实现高斯模糊一致,使用CGINCLUDE和ENDCG来组织代码,使用CGINCLUDE则可以通过在Pass中指定需要使用的顶点着色器和片元着色器函数名即可,就无需重复写相同的代码片段。先声明代码中需要使用的变量:

    SubShader
    {
		CGINCLUDE
		
        #include "UnityCG.cginc"
		// 声明代码中需要使用的变量
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		sampler2D _Bloom;
		float _LuminanceThreshold;
		float _BlurSize;

        ENDCG
    }

然后继续在CGINCLUDE和ENDCG代码块里面定义提取较亮区域所使用的顶点着色器的输出结构体v2f和对应的顶点着色器函数vertExtractBright:

		// 提取较亮区域所使用顶点着色器输出结构体
		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;
		}

接着定义一个函数luminance来计算像素的亮度值:

		// 通过明亮度公式计算得到像素的亮度值
		fixed luminance(fixed4 color) {
			return  0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; 
		}

之后再定义提取较亮区域所使用的片元着色器,在这个片元着色器中对渲染纹理进行采样,然后调用函数luminance计算亮度,再减去阈值得到提取后的亮部区域。

		// 提取较亮区域所使用的片元着色器函数
		fixed4 fragExtractBright(v2f i) : SV_Target {
			// 贴图采样
			fixed4 c = tex2D(_MainTex, i.uv);
			// 调用luminance得到采样后像素的亮度值,再减去阈值
			// 并使用clamp函数将结果截取在[0,1]范围内
			fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
			// 将val与原贴图采样得到的像素值相乘,得到提取后的亮部区域
			return c * val;
		}

还需要将提取后的亮部区域与原图像进行一个混合,因此这次再定义一个对应的顶点着色器的输出结构体v2fBloom和顶点着色器函数vertBloom,由于OpenGL与DirectX的差异性,在顶点着色器函数中需要对渲染纹理的纵坐标进行反转处理。

		// 混合亮部图像所使用顶点着色器输出结构体
		struct v2fBloom {
			float4 pos : SV_POSITION; 
			half4 uv : TEXCOORD0;
		};

		// 混合亮部图像所使用的顶点着色器函数
		v2fBloom vertBloom(appdata_img v) {
			// 顶点变换
			v2fBloom o;
			o.pos = UnityObjectToClipPos (v.vertex);
			// xy分量为_MainTex的纹理坐标,zw分量为_Bloom的纹理坐标
			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);
		} 

至此,CGINCLUDE和ENDCG代码块内的代码已全部完成,之后需要开启深度测试,关闭剔除和深度写入:

ZTest Always Cull Off ZWrite Off

接下来,到了最重要的部分,依次调用四个Pass来完成Bloom效果的实现,首先是第一个Pass,其对应的顶点着色器和片元着色器函数名是先前已经定义好的,这个Pass用来提取图像中较亮的区域;第二个和第三个Pass则是对较亮区域进行高斯模糊处理,此处使用了UsePass语义来直接使用其他Shader中的Pass,这里调用了上一篇文章中高斯模糊在竖直方向和水平方向上的两个Pass;最后一个Pass很简单了,将高斯模糊后的较亮区域与原图进行混合来得到最终的Bloom效果。

		// 第一个Pass,用来提取图像中较亮的区域
		Pass{
			CGPROGRAM
			#pragma vertex vertExtractBright
			#pragma fragment fragExtractBright
			ENDCG
		}

		// 第二个Pass,通过使用UsePass语义直接使用其他Shader中的Pass
		// 这里使用的是高斯模糊Shader下实现竖直方向上的高斯模糊的Pass
		UsePass "Unlit/Chapter12/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"

		// 第三个Pass,实现水平方向上的高斯模糊
		UsePass "Unlit/Chapter12/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"

		// 第四个Pass,将亮部图像与原图进行混合得到最终的Bloom效果
		Pass{
			CGPROGRAM
			#pragma vertex vertBloom
			#pragma fragment fragBloom
			ENDCG
		}

保存Shader,可以看见已经产生Bloom效果了,云层较亮部分的光线就会扩散到周围,让整体看起来更加明亮,同时具有一种朦胧效果。之后可以调整迭代次数(Iterations)、模糊范围(Blur Spread)、下采样数值(Down Sample)和亮度阈值(Luminance Threshold)来查看不同的Bloom效果。

[s201_bai id="15"]

一个基本的Bloom效果实现过程就到此结束。之后还会补充如何实现局部的Bloom效果,并且通过角色或场景作为例子进行详细的说明。

(未完待续...)

2020年6月29日 By Neptune

届ける言葉を今は育ててる
最后更新于 2020-10-03