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. drf JWT认证模块与自定制

    JWT模块 在djangorestframework中,有一款扩展模块可用于做JWT认证,使用如下命令进行安装: pip install djangorestframework-jwt 现在,就让我们 ...

  2. 【Kata Daily 190912】Alphabetical Addition(字母相加)

    题目: Your task is to add up letters to one letter. The function will be given a variable amount of ar ...

  3. php映射echarts柱状图

    多种样式柱状图 前台部分 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&q ...

  4. php 之 excel导出导入合并

    <?php class Excel extends Controller { //直属高校 public function __construct() { parent::Controller( ...

  5. Jmeter(二十六) - 从入门到精通 - 搭建开源论坛JForum(详解教程)

    1.简介 今天这篇文章主要是给大家讲解一下,如何部署测试环境,这里宏哥部署一个开源测论坛,后边的文章中会用到这个论坛,并且也看到童鞋们在群里讨论如何在开发将测试包发给你以后,你如何快速地部署测试环境. ...

  6. vuex和axios的基本操作

    1.在src目录下创建一个api 是用于集中处理axios的相关配置 index.js就是处理axios的文件 具体如何使用axios 还请百度axios 2.URLs.js是存放需要请求的地址的 3 ...

  7. Core WebApi项目快速入门(一):环境部署

    1.WebApi新建与部署 1.1 新建Core WebApi工程 1.2 部署 1.2.1 IIS部署 首先以文件方式发布应用程序,然后下载依赖.net core运行时及host安装包 在iis中看 ...

  8. Spring Boot 2.4 正式发布,重大调整!!!

    大家周末愉快啊,Spring Boot 2.3.5 没发布几天,你看,还是 1 周前发布的: 昨天又有粉丝留言说 Spring Boot 2.4.0 已经发布了: 我了个去,栈长赶紧跑到 Spring ...

  9. BPMN开源工作流编辑器bpmn-js落地实践中文文档

    BPMN是一套标准的业务流程建模符号规范,bpmn-js是基于此规范实现的一套渲染工具包和web建模器,可以实现拖拽生成工作流程图,效果大概如下 最近刚好用到,研究之后写了系列文章,分享给有需要的小伙 ...

  10. Nacos 多环境配置

    本文探讨Nacos作为配置中心,如何实现不同环境(开发.测试.灰度.正式)的配置管理问题 就像Maven用groupId.artifactId.version三者来定位jar包在仓库中的位置一样,Na ...