webgl智慧楼宇发光效果算法系列之高斯模糊

如果使用过PS之类的图像处理软件,相信对于模糊滤镜不会陌生,图像处理软件提供了众多的模糊算法。高斯模糊是其中的一种。

在我们的智慧楼宇的项目中,要求对楼宇实现楼宇发光的效果。 比如如下图所示的简单楼宇效果:

building.gif

楼宇发光效果需要用的算法之一就是高斯模糊。

高斯模糊简介

高斯模糊算法是计算机图形学领域中一种使用广泛的技术, 是一种图像空间效果,用于对图像进行模糊处理,创建原始图像的柔和模糊版本。
使用高斯模糊的效果,结合一些其他的算法,还可以产生发光,光晕,景深,热雾和模糊玻璃效果。

高斯模糊的原理说明

图像模糊的原理,简单而言,就是针对图像的每一个像素,其颜色取其周边像素的平均值。不同的模糊算法,对周边的定义不一样,平均的算法也不一样。 比如之前写#过的一篇文章,webgl实现径向模糊,就是模糊算法中的一种。

均值模糊

在理解高斯模糊之前,我们先理解比较容易的均值模糊。所谓均值模糊
其原理就是取像素点周围(上下左右)像素的平均值(其中也会包括自身)。如下图所示:


image.png

可以看出,对于某个像素点,当搜索半径为1的时候,影响其颜色值的像素是9个像素(包括自己和周边的8个像素)。假设每个像素对于中心像素的影响都是一样的,那么每个像素的影响度就是1/9。如下图所示:


卷积核

上面这个3*3的影响度的数字矩阵,通常称之为卷积核。

那么最终中心点的值的求和如下图所示:


image.png

最终的值是:

(8 *  1 + 1 * 2 / (8 + 1) ) = 10/9

当计算像素的颜色时候,对于像素的RGB每一个通道都进行的上述平均计算即可。

上面的计算过程就是一种卷积滤镜。所谓卷积滤镜,通俗来说,就是一种组合一组数值的算法。

如果搜索半径变成2,则会变成25个像素的平均,搜索半径越大,就会越模糊。像素个数与搜索半径的关系如下:

(1 + r * 2)的平方 // r = 1,结果为9,r=2,结果为25,r=3 结果为49.

通常 NxN会被称之卷积核的大小。比如3x3,5x5。

在均值模糊的计算中,参与的每个像素,对中心像素的贡献值都是一样的,这是均值模糊的特点。也就是,每个像素的权重都是一样的。

正态分布

如果使用简单平均,显然不是很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。

正态分布整好满足上述的的分布需求,如下图所示:


正态分布

可以看出,正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。

在计算平均值的时候,我们只需要将"中心点"作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。

高斯函数

高斯函数是描述正态分布的数学公式。公式如下:

一维高斯函数

其中,μ是x的均值,可以理解为正态分布的中心位置,σ是x的方差。因为计算平均值的时候,中心点就是原点,所以μ等于0。


一维高斯函数

如果是二维,则有:


二维高斯函数

可以看出二维高斯函数中,x和y相对是独立的。也就是说:

G(x,y) = G(x) + G(y)

这个特性的好处是,可以把二维的高斯函数,拆解成两个独立的一维高斯函数。可以提高效率。实际上,高斯模糊运用的一维高斯函数,而不是使用二维。

高斯模糊

高斯模糊的原理和前面介绍的均值模糊的原理基本上一样,只是均值模糊在计算平均值的时候,周边像素的权重都是一样的。而高斯模糊下,周边像素的权重值却使用高斯函数进行计算,这也是高斯模糊的之所以被称为高斯模糊的原因。

比如当σ取值为则模糊半径为1的权重矩阵如下:


高斯权重矩阵

这9个点的权重总和等于0.4787147,如果只计算这9个点的加权平均,还必须让它们的权重之和等于1,因此上面9个值还要分别除以0.4787147,得到最终的权重矩阵。

高斯权重矩阵

渲染流程

了解了高斯模糊的基本原理之后,来看看高斯模糊在webgl中基本渲染流程:

  1. 首先,按照正常流程把场景或者图像渲染到一个纹理对象上面,需要使用FrameBuffer功能。
  2. 对纹理对象进行施加高斯模糊算法,得到最终的高斯模糊的纹理对象。

上面第二部,施加高斯模糊算法,一般又会分成两步:

  1. 先施加垂直方向的高斯模糊算法;
  2. 在垂直模糊的基础上进行水平方向的高斯模糊算法。
    当然,也可以先水平后垂直,结果是一样的。 分两步高斯模糊算法和一步进行两个方向的高斯模糊算法的结果基本是一致的,但是却可以提高算法的效率。 有人可能说,多模糊了一步,为啥还提高了效率。 这么来说吧,如果是3x3大小的高斯模糊:
    分两步要获取的像素数量是 3 + 3 = 6; 而一步却是3 x 3 = 9。 如果是5x5大小的高斯模糊:分两步要获取的像素数量是 5+5=10; 而一步却是5 x 5=25 。显然可以算法执行效率。

渲染流程代码

对于第一步,首先是渲染到纹理对象,这输入渲染到纹理的知识,此处不再赘述,大致大代码结构如下:
···
frameBuffer.bind();
renderScene();
frameBuffer.unbind();
···

把renderScene放到frameBuffer.bind之后,会把场景绘制到frameBuffer关联的纹理对象上面。

然后是第二步,执行高斯模糊算法进行

pass(params={},count = 1,inputFrameBuffer){
        let {options,fullScreen } = this;
        inputFrameBuffer = inputFrameBuffer || this.inputFrameBuffer;
        let {gl,gaussianBlurProgram,verticalBlurFrameBuffer,horizontalBlurFrameBuffer} = this;
        let {width,height} = options;    

        gl.useProgram(gaussianBlurProgram);
        if(width == null){
          width = verticalBlurFrameBuffer.width;
          height = verticalBlurFrameBuffer.height;
        }
        verticalBlurFrameBuffer.bind();
        fullScreen.enable(gaussianBlurProgram,true);
        gl.activeTexture(gl.TEXTURE0 + inputFrameBuffer.textureUnit); //  激活gl.TEXTURE0
        gl.bindTexture(gl.TEXTURE_2D, inputFrameBuffer.colorTexture); // 绑定贴图对象
        gl.uniform1i(gaussianBlurProgram.uColorTexture, inputFrameBuffer.textureUnit);
        gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
        gl.uniform2fv(gaussianBlurProgram.uDirection,[0,1]); // 垂直方向
        gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 3); 
        gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
        gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);
    

        fullScreen.draw();
        verticalBlurFrameBuffer.unbind();

        if(horizontalBlurFrameBuffer){  // renderToScreen
          horizontalBlurFrameBuffer.bind(gl);
        }
        gl.activeTexture(gl.TEXTURE0 + verticalBlurFrameBuffer.textureUnit); //  激活gl.TEXTURE0
        gl.bindTexture(gl.TEXTURE_2D, verticalBlurFrameBuffer.colorTexture); // 绑定贴图对象
        gl.uniform1i(gaussianBlurProgram.uColorTexture, verticalBlurFrameBuffer.textureUnit);
        gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
        gl.uniform2fv(gaussianBlurProgram.uDirection,[1,0]); // 水平方向
        gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 2); 
        gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
        gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);

        fullScreen.draw();
        if(horizontalBlurFrameBuffer){
          horizontalBlurFrameBuffer.unbind();
        }
        if(count > 1){
          this.pass(params,count - 1,this.horizontalBlurFrameBuffer);
        }
        return horizontalBlurFrameBuffer;
        
    }

其中inputFrameBuffer 是第一步渲染时候的frameBuffer对象,作为输入参数传递过来。 然后开始执行垂直方向的高斯模糊算法,

verticalBlurFrameBuffer.bind();
        fullScreen.enable(gaussianBlurProgram,true);
        gl.activeTexture(gl.TEXTURE0 + inputFrameBuffer.textureUnit); //  激活gl.TEXTURE0
        gl.bindTexture(gl.TEXTURE_2D, inputFrameBuffer.colorTexture); // 绑定贴图对象
        gl.uniform1i(gaussianBlurProgram.uColorTexture, inputFrameBuffer.textureUnit);
        gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
        gl.uniform2fv(gaussianBlurProgram.uDirection,[0,1]); // 垂直方向
        gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 3); 
        gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
        gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);
    

        fullScreen.draw();
        verticalBlurFrameBuffer.unbind();

在之后执行水平方向的模糊算法:

 if(horizontalBlurFrameBuffer){  // renderToScreen
          horizontalBlurFrameBuffer.bind(gl);
        }
        gl.activeTexture(gl.TEXTURE0 + verticalBlurFrameBuffer.textureUnit); //  激活gl.TEXTURE0
        gl.bindTexture(gl.TEXTURE_2D, verticalBlurFrameBuffer.colorTexture); // 绑定贴图对象
        gl.uniform1i(gaussianBlurProgram.uColorTexture, verticalBlurFrameBuffer.textureUnit);
        gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
        gl.uniform2fv(gaussianBlurProgram.uDirection,[1,0]); // 水平方向
        gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 2); 
        gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
        gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);

        fullScreen.draw();
        if(horizontalBlurFrameBuffer){
          horizontalBlurFrameBuffer.unbind();
        }

shader 代码

shader 代码分成两部分,一个顶点着色器代码:

const gaussianBlurVS =  `
  attribute vec3 aPosition;
  attribute vec2 aUv;
  varying vec2 vUv;
  void main() {
    vUv = aUv;
    gl_Position = vec4(aPosition, 1.0);
  }
`;

另外一个是片元着色器代码:

const gaussianBlurFS = `
precision highp float;
precision highp int;
#define HIGH_PRECISION
#define SHADER_NAME ShaderMaterial
#define MAX_KERNEL_RADIUS 49
#define SIGMA 11
varying vec2 vUv;
uniform sampler2D uColorTexture;
uniform vec2 uTexSize;
uniform vec2 uDirection;
uniform float uExposure;
uniform bool uUseLinear;
uniform float uRadius;

float gaussianPdf(in float x, in float sigma) {
  return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
}
void main() {
  vec2 invSize = 1.0 / uTexSize;
  float fSigma = float(SIGMA);
  float weightSum = gaussianPdf(0.0, fSigma);
  vec4 diffuseSum = texture2D( uColorTexture, vUv).rgba * weightSum;
  float radius = uRadius;

  for( int i = 1; i < MAX_KERNEL_RADIUS; i ++ ) {
    float x = float(i);
    if(x > radius){
      break;
    }
    float gaussianPdf(x, fSigma),t = x;
    vec2 uvOffset = uDirection * invSize * t;
    vec4 sample1 = texture2D( uColorTexture, vUv + uvOffset).rgba;
    vec4 sample2 = texture2D( uColorTexture, vUv - uvOffset).rgba;
    diffuseSum += (sample1 + sample2) * w;
    weightSum += 2.0 * w;
   
  }
  vec4 result = vec4(1.0) - exp(-diffuseSum/weightSum * uExposure);
  gl_FragColor = result;
}
`

最终渲染的效果如下:


image.png

应用案例

目前用到的主要是发光楼宇的效果。 下面是几个案例图,分享给大家看看:


案例1
案例2

参考文档

http://www.ruanyifeng.com/blog/2012/11/gaussian_blur.html

如果对可视化感兴趣,可以和我交流,微信541002349. 另外关注公众号“ITMan彪叔” 可以及时收到更多有价值的文章。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容