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

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

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

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

高斯模糊简介

高斯模糊算法是计算机图形学领域中一种使用广泛的技术, 是一种图像空间效果,用于对图像进行模糊处理,创建原始图像的柔和模糊版本。

使用高斯模糊的效果,结合一些其他的算法,还可以产生发光,光晕,景深,热雾和模糊玻璃效果。

高斯模糊的原理说明

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

均值模糊

在理解高斯模糊之前,我们先理解比较容易的均值模糊。所谓均值模糊

其原理就是取像素点周围(上下左右)像素的平均值(其中也会包括自身)。如下图所示:

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

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

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



最终的值是:

(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;
}
`

最终渲染的效果如下,案例中渲染的是一个球体的线框:

应用案例

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

当然还有更多的应用场景,读者可以自行探索。

参考文档

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

结语

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

webgl智慧楼宇发光效果算法系列之高斯模糊的更多相关文章

  1. 基于 HTML5 WebGL 的智慧楼宇可视化系统

    前言 可视化的智慧楼宇在 21 世纪是有急迫需求的,中国被世界称为"基建狂魔",全球高层建筑数量位居首位,所以对于楼宇的监控是必不可少.智慧楼宇可视化系统更多突出的是管理方面的功能 ...

  2. 基于 HTML5 WebGL 的智慧楼宇三维可视化监控

    前言 可视化的智慧楼宇在 21 世纪是有急迫需求的,中国被世界称为"基建狂魔",全球高层建筑数量位居首位,所以对于楼宇的监控是必不可少.智慧楼宇可视化系统更多突出的是管理方面的功能 ...

  3. 使用three.js(webgl)搭建智慧楼宇、设备检测、数字孪生——第十三课

    老子云:有道无术,术尚可求,有术无道,止于术. 咱开篇引用老子的话术,也没其它意思,只是最近学习中忽有感悟,索性就写了上来. 这句话用现代辩证思维理解,这里的"道" 大抵是指方法论 ...

  4. 基于 HTML5 WebGL 的 智慧楼宇能源监控系统

    前言 21世纪,在能源危机和全球气候变暖的压力下,太阳能等可再生能源越来越受到关注,其中光伏建筑一体化逐渐成为绿色发展方式和生活方式,加强节能降耗,支持低碳产业和新能源.可再生能源发展,也已经成为国家 ...

  5. 【C#实现漫画算法系列】-判断 2 的乘方

    微信上关注了算法爱好者这个公众号,有一个漫画算法系列的文章生动形象,感觉特别好,给大家推荐一下(没收过广告费哦),原文链接:漫画算法系列.也看到了许多同学用不同的语言来实现算法,作为一枚C#资深爱好的 ...

  6. JAVA算法系列 冒泡排序

    java算法系列之排序 手写冒泡 冒泡算是最基础的一个排序算法,简单的可以理解为,每一趟都拿i与i+1进行比较,两个for循环,时间复杂度为 O(n^2),同时本例与选择排序进行了比较,选择排序又叫直 ...

  7. JAVA算法系列 快速排序

    java算法系列之排序 手写快排 首先说一下什么是快排,比冒泡效率要高,快排的基本思路是首先找到一个基准元素,比如数组中最左边的那个位置,作为基准元素key,之后在最左边和最右边设立两个哨兵,i 和 ...

  8. javascript实现数据结构与算法系列:栈 -- 顺序存储表示和链式表示及示例

    栈(Stack)是限定仅在表尾进行插入或删除操作的线性表.表尾为栈顶(top),表头为栈底(bottom),不含元素的空表为空栈. 栈又称为后进先出(last in first out)的线性表. 堆 ...

  9. 三白话经典算法系列 Shell排序实现

    山是包插入的精髓排序排序,这种方法,也被称为窄增量排序.因为DL.Shell至1959提出命名. 该方法的基本思想是:先将整个待排元素序列切割成若干个子序列(由相隔某个"增量"的元 ...

随机推荐

  1. Tarjan缩点入门

    缩点 顾名思义,缩点就是把一个强连通分量缩成一个点 Tarjan 在dfs的过程中记录时间戳,若能够通过某个点返回已遍历的点,则可以缩点 inline void Tarjan(int x)// st栈 ...

  2. 4G DTU采用的4G通信模块介绍

      4g通信模块一种基于4G网络进行数据传输的工业级通讯终端,其主要作用是将采集到的传感器数据.仪表数据,传输至服务器/上位机.监控中心.众山研发生产的4g无线通讯设备--4G DTU是一款物联网数据 ...

  3. Java线程池原理及分析

    线程池是很常用的并发框架,几乎所有需要异步和并发处理任务的程序都可用到线程池. 使用线程池的好处如下: 降低资源消耗:可重复利用已创建的线程池,降低创建和销毁带来的消耗: 提高响应速度:任务到达时,可 ...

  4. 2018-12-7 CSAPP及C++

    今天虽然起床迟,但从结果上来看,学习效率还算不赖.从这几天的状况来看,为记录晚上上床后的学习内容,决定把在床上的学习内容算在后一天的学习中.那么从现在开始就可以协商英语的半个小时100个单词了. 英语 ...

  5. Navicat无法直连MySQL怎么办?

    本文背景 Navicat是图形化操作MySQL的强大工具,但是当数据库的服务器没有开放3306端口给办公网络时,在办公网使用navicat连接数据库是连不上的.要操作数据库,只能先ssh登陆到数据库服 ...

  6. 9_Palindrome Number

    9.Palindrome Number Determine whether an integer is a palindrome. An integer is a palindrome when it ...

  7. 7_Reverse Integer

    7.Reverse Integer Given a 32-bit signed integer, reverse digits of an integer. Example 1: Input: 123 ...

  8. Android Google官方文档解析之——Application Fundamentals

    Android apps are written in the java programming language.The Android SDK tools compile your code-al ...

  9. 【python】将变量保存在本地及读取

    在用jupyter notebook写python代码的过程中会产生很多变量,而关闭后或者restart jupyter kernel后所有变量均会消失,想要查看变量就必须将代码重新再运行一遍,而想在 ...

  10. 深入浅出 webpack 之基础配置篇

    前言 前端工程化经历过很多优秀的工具,例如 Grunt.Gulp.webpack.rollup 等等,每种工具都有自己适用的场景,而现今应用最为广泛的当属 webpack 打包了,因此学习好 webp ...