Skip to main content

炫光(Bloom)

焦点不完美(胶片与眼睛)

泛光效果是由光学设计缺陷产生的。这种现象在镀膜质量欠佳的旧镜头中尤为显著。当光线通过这些透镜时,寄生照明现象会在内部产生,并随后在薄膜乳剂层中发生散射。最终,漫反射光晕会出现在光源周围、场景的明亮区域以及高对比度边缘。

虽然泛光效果与胶片本身没有直接关联,但正是胶片的特性使其呈现出独特的视觉魅力。额外的光散射源于乳剂层的轻微不均匀性,当卤化银形成晶粒簇时,这种光散射效应会随着显影过程进一步加剧。

因此,原本平滑规则的光学表现逐渐变得非线性且富有层次,展现出更自然的视觉效果。

使用老式镜头、特殊光学镜头或散景滤镜(如Pro-Mist)时,泛光效果最为显著。由于这种效果的产生机制既涉及光学系统也依赖胶片乳剂特性,因此在采用小画幅胶片(特别是8毫米胶片)的拍摄场景中尤为突出:

image.png

8 mm 电影胶片扫描

泛光现象的一个物理基础在于,现实中的镜头永远无法实现完美聚焦。即使是理论上的完美镜头,也会将入射图像与艾里斑(由点光源通过圆形孔径产生的衍射图案)进行卷积处理。在常规条件下,这种光学瑕疵并不明显,但当遇到强光源时,这些瑕疵就会显著显现。因此,强光物体的影像会呈现出超越其自然边界的视觉效果。

艾里斑函数具有快速衰减的特性,但其尾部扩散范围极宽(理论上具有无限宽度)。当图像相邻区域的亮度值处于相近范围时,艾里斑引起的模糊效应并不显著;但在明亮区域与暗部区域交界处,艾里斑的尾部会变得可见,并且其影响范围可以远远超出原亮部区域。

在HDR图像处理过程中,可以通过在像素值归一化前应用特定算法来模拟该现象:对于高精度镜头使用艾里斑窗函数进行卷积运算,或通过高斯模糊算法模拟普通镜头的成像效果。传统非HDR成像系统无法完整复现该效果,因为光晕溢出程度与光源亮度呈正相关关系。

例如在室内拍摄时,透过窗户可见的室外景物亮度可能达到室内物体的70-80倍。当相机按照室内场景设定曝光参数时,窗外高亮区域的影像经艾里斑卷积处理后,就会产生光晕溢出窗框的视觉效果。

艾里斑

艾里斑是点光源通过衍射受限成像时,由于衍射而在焦点处形成的光斑。中央是明亮的圆斑,周围有一组较弱的明暗相间的同心环状条纹,把其中以第一暗环为界限的中央亮斑称作艾里斑。这个光斑的大小可以用下面的公式来估计:

image.png

image.png

CCD 传感器饱和度(数码相机)

CCD 图像中的光晕

数码相机中的光晕是由光电二极管中的电荷溢出引起的,光电二极管是相机图像传感器中的感光元件。[3]当光电二极管暴露在非常明亮的光源下时,积累的电荷会溢出到相邻像素中,从而产生光晕效果。这被称为“电荷出血”。

泛光效果在像素较小的相机中更为明显,因为电荷消散的空间较小。高 ISO 设置也会加剧这种情况,这会增加相机对光线的敏感度,并可能导致更多的电荷积累。

计算机中的泛光

泛光(有时称为光泛光或发光)是一种计算机图形效果,用于视频游戏、演示和高动态范围渲染 (HDRR),用于再现真实摄像机的成像伪影。该效果会产生从图像中明亮区域的边界延伸的光条纹(或羽化),从而产生极亮的光线压倒相机或眼睛捕捉场景的错觉。在 Tron 2.0 的作者于 2004 年发表了一篇关于该技术的文章后,它被广泛用于视频游戏。

image.png

实现思路

  • 1.首先根据一个阙值提出图像中的较亮区域,并将它们存储在一张渲染纹理中
  • 2.利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果
  • 3.与原图混合

前置知识

  • HDR与LDR
  • 高斯模糊
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的地方

高斯模糊(Gaussian Blur)

  • 一种图像模糊
  • 减少图像噪声,降低细节层次

公式:

QQ_1743443987447.png

σ 是标准方差(一般取值为1),x和y对应了当前位置到卷积核中心的整数距离,只需要计算高斯核中各个位置对应的高斯值,需要对高斯值进行归一化,让每个权重除以所有权重的和,

  • 卷积
    • 一种图像操作
    • 需要一个卷积核(3*3等等)

Feature Map的值为0是

QQ_1743444056170.png

  • 每个位置对应相乘,最后相加

  • 计算高斯核
  • 计算核中心(0,0)
  • 核大小
  • 标准方差:1.5

归一化的作用是:为了保证卷积后的图像不会变暗

算法实现

  • 调用onRenderImage函数
  • Shader:使用3个Pass完成Bloom效果

入门精要Bloom的实现

命名为Gaussian但是实现的是bloom效果
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Gaussian : PostEffectsBase
{

    public Shader GaussianShader;
    private Material GaussianMat = null;
    // Blur iterations - larger number means more blur.
    [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);
            return GaussianMat;
        }
    }
    protected void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (material != null) {
            //设置亮度阙值
            material.SetFloat("_LuminanceThreshold", luminanceThreshold);
            //定义长宽高
            int rtW = source.width / downSample;
            int rtH = source.height / downSample;
            //定义buffer0缓冲区的大小,设置buffer0的过滤设置,并存储buffer0
            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0);
            buffer0.filterMode = FilterMode.Bilinear;
            Graphics.Blit(source, buffer0, material, 0);
            //for循环,iteration次数
            for (int i = 0; i < iterations; i++)
            {
                ///for:
                ///设置blurSize
                ///新定义RenderTexture buffer1
                ///Render the vertical pass
                ///release buffer0
                ///buffer1赋值给buffer0
                ///re-define buffer1
                ///Render the horizontal pass
                ///释放buffer0
                ///buffer1赋值给buffer0
                material.SetFloat("_BlurSize",1.0f + blurSpread*i);
                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
                Graphics.Blit(buffer0,buffer1,material,1);
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
                buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
                Graphics.Blit(buffer0, buffer1, material, 2);
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
            }
            //mat中将buffer0赋予给_Bloom
            material.SetTexture("_Bloom",buffer0);
            //渲染图像到des上
            Graphics.Blit(source,destination,material,3);
            //释放buffer0
            RenderTexture.ReleaseTemporary(buffer0);
        }
        else
        {
            Graphics.Blit(source,destination);
        }
    }
}
Shader "Custom/Guassian"
{
    Properties {
        //定义贴图材质 2D
        //_Bloom值 2D
        //亮度阙值,float
        //模糊尺寸 Float
        _MainTex ("Base (RGB)", 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;

        //定义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; 
        }
        //提取亮度的片元着色器
        ///先进行纹理采样
        ///进行亮度提取,采样得到的亮度值减去阙值,并截取到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;
        };

        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 {
            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
}

参考文章