netcy

netcy 查看完整档案

武汉编辑  |  填写毕业院校  |  填写所在公司/组织 xiaozhuanlan.com/webgl 编辑
编辑

欢迎对canvas、webgl、图形学感兴趣的读者订阅专栏。
点击下面链接可以订阅:
[canvas高级进阶] https://xiaozhuanlan.com/canvas
[webgl入门到高级进阶]https://xiaozhuanlan.com/webgl

个人动态

netcy 发布了文章 · 1月18日

canvas多重阴影发光效果

canvas多重阴影发光效果

前言

在一个项目中,客户提了一个发光的效果,效果图如下:

阴影

有的人可能会说,这个用阴影其实就可以实现。但是从图中可以看出,是一个比较强烈的发光效果。实际的应用过程中我们会发现用简单阴影参数实现的效果很难达到这样强烈的发光效果。
比如

ctx.shadowColor = 'rgba(255,0,0,1)';
  ctx.shadowBlur =10;
  ctx.shadowOffsetX = 10;
  ctx.shadowOffsetY = 10;

  ctx.fillStyle = 'rgba(0,0,255,1.0)';
  ctx.fillRect(100,100,200,100);

简单的阴影效果下,shadowBlur 表示阴影半径。当阴影半径比较大的时候,阴影的扩散程度会比较大,但阴影的强烈度不够。在阴影的半径比较小的时候,阴影的强烈度是够的,但阴影的扩散程度会比较小。

多重阴影

如何达到具有较强的阴影强度,又有较好的阴影扩散度呢?也就是实现这种比较强烈的发光效果。嗯,答案就是使用多重阴影效果。

所谓多重阴影效果,使用阴影效果对图形进行多次绘制,多次绘制的过程中,shadowBlur的值会不一样,这样可以形成多个阴影叠加的效果。

下面是一个简单的示例,代码如下。

ctx.shadowColor = 'rgba(255,255,0,1)';
  ctx.shadowBlur = 20;
  ctx.shadowOffsetX = 10100;
  ctx.shadowOffsetY = 10100;

  ctx.beginPath();
  ctx.fillStyle = 'rgba(0,0,255,1.0)';
  ctx.arc(-10000, -10000, 50, 0, Math.PI * 2);
  ctx.fill();

  ctx.shadowColor = 'rgba(255,0,0,1)';
  ctx.shadowBlur = 20;
  ctx.shadowOffsetX = 10100;
  ctx.shadowOffsetY = 10100;

  ctx.beginPath();
  ctx.fillStyle = 'rgba(0,0,255,1.0)';
  ctx.arc(-10000, -10000, 30, 0, Math.PI * 2);
  ctx.fill();

从代码中我们可以看出我们多次使用了阴影的绘制啊,最终的绘制效果如下图所示。

从图中可以看出, 阴影有较好的扩散程度,也有较好的强烈度。

下面是用多重阴影实现的文字霓虹灯效果,同样可以看出有较好的发光效果。

总结

可以看出要达到强烈的发光效果, 需要使用多重阴影功能。当然使用多种阴影也不是没有限制的, 因为阴影本身有很大的性能损耗。通过尝试我们发现一般3~5次之间就能够达到较好的效果吧。

如果对可视化感兴趣,可以和我交流,微信541002349.

查看原文

赞 0 收藏 0 评论 0

netcy 发布了文章 · 1月9日

智慧园区可视化设计思路

智慧园区可视化设计思路

前言

随着物联网技术的不断发展,推动了园区的智能化管理与服务不断升级。科技的进步激发市场诉求,智能化园区的市场规模也越来越大。
在经济快速发展和政府政策的推动下,伴随着国内智慧城市建设步伐的加快,园区规划建设整体性越来越强,更加注重各种基础配套设施,以更好的服务促进高新产业的发展。尤其是注意产业园区的信息化建设,构建互联互通、资源共享的信息资源化网络。
智慧园区是在园区搭建的IT基础设施之上,运用物联网、云计算、多媒体等现代信息技术,帮助园区在信息化建设方面构建统一的组织管理协调架构、业务管理平台和对外服务平台,为园区管理者以及企业提供创新管理与运营服务。
我们团队对于智慧园区可视化项目做的已非常成熟,在本文中我会采用一些我们团队做过的比较通用又具有代表性的智慧园区项目来阐述我的设计思路。

一、确认风格

园区项目大致可以区分为科技风和写实风。
以下都是我们团队做过的项目案例,可以很明显的看出风格上的差异。风格的确立至关重要,直接影响了后期的UI设计以及建模。
科技风格更能抓住用户眼球,给人带来酷炫的感觉。写实风格更加追求场景的真实性和还原性,两者没有优劣,根据需求的不同进行选择即可。
企业微信截图_16093166989950.png企业微信截图_16093232552253.png

企业微信截图_16093245626192.png

二、模块划分

在智慧园区项目中,我们要将需求规划为各个不同的模块,呈现不同的数据和功能点。
在以下要分享的案例中,我们把它分成了五个模块去呈现,分别是:

智能概览

在这个模块中我们需要去展示出园区各个设备的总量以及告警数量,在数据的宏观层面给到用户更为直观的感受,以便让用户能从中更快速的获取到他想要的信息。

智能安防

在智能安防模块中,不需要过多的去展示数据层面,我们把重点放在了监控上。园区以及大楼内的每个监控点位按照实际场景还原,可以通过点击3D场景中的监控点位来查看相应点位的监控,放大呈现每个点位的实时情况,做到园区无死角,第一时间就能发现园区的异常情况并做到及时排查与处理。
接口方面可以通过rtsp视频流接口或者第三方监控系统进行视频传输对接,对于业主来说是非常方便的一种对接方式。
企业微信截图_16093987385094.png

智能出入

对于园区而言,最重要的就是安全管理。在智能出入这个模块中,汇聚了"人员出入"、"车辆出入"以及"车位利用"。对于人员和车辆的出入,我们以时间段作为划分可以更直观看出在一天中哪个时段是峰值,更利于业主在安全问题上做出较为正确的部署。
在3D场景中,园区的大门出入口、停车场出入口,大楼内的闸机、门禁可以实时呈现细节数据。大多数停车场都是只能呈现出总体和剩余车位。但如果详细到具体车位,这个信息是我们掌握不到的。所以我们在停车场车位中设计了通过颜色区分和摄像头的两者结合,具象到某一个车位。微观和宏观的结合使业主对园区内的出入情况做到了如指掌。
企业微信截图_16094012252449.png企业微信截图_16094014035039.png

智能用电

智能用电模块的关注点在于掌握整个大楼的用电情况。同样的我们在宏观的数据层面会以时间段的形式呈现出在一天中哪个时间断内是用电峰值,在总量上是否超出预期用电。微观上我们要能查看每个用电设备的详细用电数据。这些对于业主在日常维护和节省开支上会起到很大的作用。
企业微信截图_16094064975768.png

智能消防

智能消防模块汇聚了消防的设备分布,含用电设备、烟感设备、消防设备等。我们首先还是要以统计总量的形式统计告警数据。同样的在3D场景中,通过不同的设备分布呈现单个告警设备不同的具体数据展示。同时如果设备发生告警,告警设备颜色会变红,弹框自动弹出,设备附近的视频监控会自动弹出,便于能在第一时间掌握告警状态。
企业微信截图_16094064333106.png

总结

随着5G时代的到来,各种先进5G应用、智慧终端在楼宇中逐步使用、更好的协助人们提升楼宇管理效率,做好楼宇运维。通过智慧大楼的建设,实现各业务子系统数据的汇集、聚焦、展示、以展现智慧大楼在新技术汇聚应用方面的能力和效果。
这个案例是楼宇安全管理责任到人、安全风险及时发现处理、安全知识的宣讲与推广是提高商务楼宇安全管理水平的重要途经,为此引入楼宇安全管理平台/5G消息端安全应用,助力提升楼宇的安全管理水平。实现了园区的智能安全管理和数据汇总,提升了园区的智慧化建设。同时建设了统一运营平台,目的是为了将园区中所有的设备进行数据统一整合,汇总和数据化在大屏上呈现,实现了各类园区应用场景的呈现。如智能安防中,园区内所有的摄像头点位与系统中的点位进行真实还原,在大屏上就能实时掌握园区所有动态。在智能出入中,通过门禁、闸机等设备的数据采集,可以较好的对大楼内的员工以及临时出入人员还有车辆进行统一管理。
特别要说明的是,大家在做类似智慧园区可视化的项目时,不一定非得像我一样分出这几个模块。分模块以及模块内容都是根据需求而定。

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

查看原文

赞 0 收藏 0 评论 0

netcy 赞了文章 · 1月7日

白话分析自适应跟响应式的区别

根据日常开发经验及网上相关资料,用简单通俗易懂的大白话分析自适应响应式的区别。注:本文只分析自适应跟响应式的区别以及了解其由来的背景,不在于讨论其使用的方法

一、什么是自适应布局

自适应布局就是宽度自适用布局,在不同大小的设备上,网页以等比例的形式缩放宽度,呈现同样的主体内容和排版布局

自适应布局演示图

图片
随着屏幕宽度缩放,网页内容也以等比例缩放,不管屏幕宽度为多少,网页主体排版布局总是一样的

二、什么是响应式布局

响应式布局就是根据屏幕大小变化,页面的内容排版布局会自动调整变动,已呈现更好的用户体验

响应式布局演示图

图片
随着屏幕宽度的缩放,页面做出相应调整,布局和展示的内容会有所变动

1.自适应布局出现的背景

在PC时代初期,网页设计者都会设计固定宽度的页面,最开始的电脑显示器分辨率种类不多,因为当时电脑本来就少。后来随着显示器种类越来越多,以及笔记本、平板电脑的普及,这种固定宽度的页面出现了问题。于是出现了一种新的布局方式,宽度自适应布局。我们平时谈论的自适应布局,大多指的就是宽度自适应布局

再到后来,互联网大战从PC打到了手机,还有 HTML5 标准的发布。自适应布局也从PC延伸到手机,自适应布局也因此火了起来,成为网页设计的必要需求

2.响应式布局出现的背景

自适应虽然成为网页设计的必要需求,但还是暴露出一个问题,如果屏幕太小,即使网页内容能够根据屏幕大小进行适配,但是在小屏幕上查看,会感觉内容过于拥挤,降低了用户体验。此时,为了解决这个问题而衍生出来的概念就是响应式布局。它可以自动识别屏幕宽度、并做出相应调整。网页的排版布局和展示的内容会有所变动

最后

觉得文章不错的,给我点个赞哇,关注一下呗!
技术交流可关注微信公众号【GitWeb】,加我好友一起探讨
微信交流群:加好友(备注思否)邀你入群,抱团学习共进步

查看原文

赞 14 收藏 2 评论 0

netcy 发布了文章 · 2020-12-30

自动化车间3D可视化设计思路

自动化车间3D可视化设计思路

随着国内制造业企业的高速发展,再加上政策支持,高效的生产模式和先进的管理方式越来越受到企业重视。更多的企业将工业信息化技术进行广泛的应用,比如MES系统、数字孪生以及生产管理可视化等技术的研究应用。
近期我们团队利用自主开发的大屏编辑器与3D编辑器结合做了一个智能实验室三维可视化平台项目。
在这个智能实验室三维可视化平台里主要支持下列功能:

  • 智能巡检
  • 设备自动告警
  • 实时视频监控
  • 实时数据的展示
  • 数据融合
  • 动画视角的自动切换
  • 任务的进度展示

 

全局自动巡检可视化

页面初始化后,画面视角自动移动开始智能巡检。通过动画和镜头拉近的配合可以看到每一个工位。

 

设备可视化

当巡检到设备点位时,通过3D技术,实现对设备结构、运行参数的可视化。无需耗费人力成本,即可获取设备运行状态,生产状态等详细信息。

 

告警可视化

设备发生故障时,除了通过三维可视化平台获取故障信息外,平台支持异常告警并及时推送告警信息,帮助操作人员迅速做出应对。也可以调用附近监控摄像机画面弹窗,检查是否现场操作不当或其它现场因素导致故障。

 

任务可视化

通过3D技术,可查看每个检测任务的进度情况,可以清晰的看出每个设备的的利用率以及设备与检测进度的关系。

设备动画

南瑞机械臂骨骼动画.gif
 
中间是一个三维场景,三维场景作为iframe页面嵌入在2D大屏中,所有的通信都是通过window.postMessage机制来完成。3D部分使用我们团队自主开发的3D编辑器编辑而成,两侧是一些大屏元素,使用我们团队自主开发的大屏编辑器编辑而成。

 

设计浅析

本项目和以往的项目不同。很多项目中三位大屏可视化的交互大部分都是通过点击完成交互,比如点击2D的数据3D部分会完成镜头动画动作,或者点击3D部分的设备会有billboard弹框弹出。
这个项目的不同点同时也是关键点就在于通过数据驱动去实现整个可视化系统的自动化。
如何做到通过数据驱动去推动可视化系统的自动化。
 

业务层面

很重要的一点是我们要很了解项目的业务逻辑。
首先在工序上串行还是并行,是否要按照顺序按步骤进行。比如必须是工位1完成后才能到工位2,然后工位3...依次进行。
第二点是在是否存在回头路,也就是说一个工位在一次任务中是否会涉及到多次利用。
第三点是数据之间的关联。这关乎于3D与2D之间在业务层面的交互是否符合真实场景。
毕竟任何可视化产品的基本原则都是帮助用户能更清晰的观察工业流程和实现可视化管理。
 

功能层面

在弄清楚业务层面的前提下,我们就要开始设计如何实现本项目中的需求。
我们根据CAD图纸以及视频、照片,通过建模还原实际场景。

设备动画

在可视化产品中,现在越来越多的需求不只是满足于还原真实场景这个层面。大家都希望看到的是动态的,最好是能还原真实动画的模型。这样在整个三维场景中会比静态的三维场景要更加逼真,还原度更高。所以我们会在模型中加入结合实际的设备骨骼动画,不同的设备结合真实场景中的设备会有其对应的部位动画。

巡检路线设计

实际场景还原后,根据坐标轴来设计镜头动画。我的想法是先设计一条完整的镜头动画线,这条线的路线可以看到每个工位,镜头在这条路线上来回巡检。巡检模式下的镜头角度为俯视角度,能更好的看清每个工位。

路线切分

因为要突出的是自动化,只有单纯的巡检还远远不够,在此基础上根据业务逻辑,可以在这条巡检动画路线上增加一些节点。这个节点就是工位节点,当到了这些节点时,我的想法是镜头动画会focus到具体的一个工位上,等于是把一条完成的路线进行切分。同时2D这边也会根据这个节点的数据驱动产生交互呈现业务需求。
满足这两点后,一个比较基础的三维可视化自动化平台的就已经初见雏形。

设备告警

借来下是设计设备告警。
由于设备告警这类事件是一个突发性的偶然事件,但同时从业务层面上来说它是一个紧急性事件。所以当推送的数据中有发生告警事件,那么它的优先级是相对于其他事件的优先级是要高的。所以当有告警事件发生时,2D部分在数据中会以数据标红呈现,3D部分会自动定位到发生告警的设备上,同时暂时停掉其他的镜头动画,开启设备工位旁的摄像头获取真实场景中的现场情况以便管理人员能及时做出决策。

任务进度

在以上这些的基础上,我们需要体现出每个设备在某一个任务中的进度情况。这个任务进度情况,就是上面提到的工序。每一个人任务都会有自己的工序,考虑到不同工序的问题。所以在设计这个功能需求时,我将任务中要经过的工位设备进行连线,采用流动的效果体现任务的进行。用颜色的已经亮光来表现任务的实时进度。同时同时间内不会只有一个任务在进行,这样设备的利用率太低,所以会有多个任务在进行。但是在呈现任务进度这个功能时,会根据不同的工位设备依次呈现不同的任务进度相互切换,避免3D场景眼花缭乱分辨不清。
 

总结

本次项目是一个非常好的尝试,利用数据驱动三维可视化的自动运转与交互。当然以上我分享的只是此项目中较为有代表的一部分,整个项目构成的是一个可视化管理系统,我就不展开详细讨论了。
目前这个项目的尝试也比较成功,反馈比较好。在工业化的概念里,但凡涉及到车间、工厂、流水线、制造等诸如此类的可视化系统,都是可以用这样的设计逻辑去设计和开发。
 
如果大家有好的想法,可以微信:541002349 讨论。

查看原文

赞 1 收藏 0 评论 0

netcy 发布了文章 · 2020-12-30

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

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彪叔” 可以及时收到更多有价值的文章。

查看原文

赞 0 收藏 0 评论 0

netcy 发布了文章 · 2020-12-29

canvas可视化效果之内阴影效果

canvas可视化效果之内阴影效果

楔子

在之前的一个轨道交通可视化项目中,运用到了很多绘制技巧。 可以参考 之前的一篇文章 《利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果

效果图中的轨道,就同时存在外发光和内发光效果的效果。

外发光效果

我们知道外发光效果是很容易实现的,直接通过设置阴影效果即可达到。比如我们随便绘制一条线段,加上阴影效果,看起来就是外发光的效果:

      ctx.clearRect(0,0,canvas.width,canvas.height);
      ctx.shadowBlur= 20;
      ctx.shadowOffsetX = 0;
      ctx.shadowOffsetY = 0;
      ctx.shadowColor="red";
      ctx.lineCap = "round";
      ctx.lineJoin  = "round";
      ctx.lineWidth = 10;
      ctx.strokeStyle = "blue";
      ctx.beginPath();
      ctx.moveTo(300,300);
      ctx.lineTo(750,300);
      ctx.quadraticCurveTo(800,300,800,350);
      ctx.lineTo(800,450);
      ctx.quadraticCurveTo(800,500,750,500);
      ctx.lineTo(300,500);
      ctx.stroke();

效果图如下:

如果绘制圆形效果如下:

上面的代码都容易理解,就是通过shadowBlur产生渐变阴影的效果。 默认的阴影,我们称之为外阴影,意思都是图像向往展开的阴影效果。

内阴影

接下来的问题可能就变得有点难度。如果我们需要如下的一个内阴影的效果呢?

有人说,简单,一个渐变就搞定了。 那再看看下面这个图像呢?

还是没问题,还是可以通过渐变来搞定,只是渐变的stop设置要麻烦一点罢了。 如果在复杂一些的图形呢,比如下面的线段效果:

对于上面的线段的内阴影效果,就很难使用简单的渐变来实现了。

如何绘制内阴影效果

要实现上面的内阴影效果,首先还是使用shadowBlur参数,然后把ctx的globalCompositeOperation参数设置为“source-out” 即可。 试试如下代码:

 ctx.globalCompositeOperation = 'source-out';
     ctx.beginPath();
     ctx.beginPath();
    ctx.moveTo(300,300);
    ctx.lineTo(750,300);
    ctx.quadraticCurveTo(800,300,800,350);
    ctx.lineTo(800,450);
    ctx.quadraticCurveTo(800,500,750,500);
    ctx.lineTo(300,500);
    ctx.lineCap = "round";
     ctx.shadowBlur =15;
     ctx.lineWidth = 20;
     ctx.shadowColor="blue";
     ctx.fillStyle = 'red';
     ctx.strokeStyle = 'red';
     ctx.stroke();

最终绘制的效果就是上面的线段图的效果:

同时绘制内外阴影效果

如果修改globalCompositeOperation为“xor”,我们还可以得到既有内阴影又有外阴影的效果。 代码如下:

 ctx.globalCompositeOperation = 'xor';
     ctx.beginPath();
     ctx.beginPath();
    ctx.moveTo(300,300);
    ctx.lineTo(750,300);
    ctx.quadraticCurveTo(800,300,800,350);
    ctx.lineTo(800,450);
    ctx.quadraticCurveTo(800,500,750,500);
    ctx.lineTo(300,500);
    ctx.lineCap = "round";
     ctx.shadowBlur =15;
     ctx.lineWidth = 20;
     ctx.shadowColor="red";
     ctx.fillStyle = 'red';
     ctx.strokeStyle = 'red';
     ctx.stroke();

绘制的效果如下:

内阴影的缺陷

上述方法实现的内阴影颜色的颜色只能和绘制主体一样的颜色,而不能像外阴影的颜色一样,可以自由定义。 比如把上述代码中的shadowColor改成blue,只有外阴影的颜色改变了:

 ctx.globalCompositeOperation = 'xor';
     ctx.beginPath();
     ctx.beginPath();
    ctx.moveTo(300,300);
    ctx.lineTo(750,300);
    ctx.quadraticCurveTo(800,300,800,350);
    ctx.lineTo(800,450);
    ctx.quadraticCurveTo(800,500,750,500);
    ctx.lineTo(300,500);
    ctx.lineCap = "round";
     ctx.shadowBlur =15;
     ctx.lineWidth = 20;
     ctx.shadowColor="red";
     ctx.fillStyle = 'red';
     ctx.strokeStyle = 'red';
     ctx.stroke();

最终的效果如下图所示:

从图上可以看出只有外阴影颜色改变了,内阴影使用的本体的颜色。

实现闪烁的效果

基于上面的实现,我们可以实现一个阴影闪烁的效果,只需要不断更改shadowBlur的值,代码如下:
···
setInterval(()=>{
xor();
},10)

let shadowBlur = 5;
let offset = 0.5;



function xor(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.globalCompositeOperation = 'xor';
  ctx.shadowBlur= shadowBlur;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 0;
  ctx.shadowColor="red";
  ctx.lineCap = "round";
  ctx.lineJoin  = "round";
  ctx.lineWidth = 10;
  ctx.strokeStyle = "blue";
  ctx.beginPath();
  ctx.moveTo(300,300);
  ctx.lineTo(750,300);
  ctx.quadraticCurveTo(800,300,800,350);
  ctx.lineTo(800,450);
  ctx.quadraticCurveTo(800,500,750,500);
  ctx.lineTo(300,500);
  ctx.stroke();
  // ctx.stroke();
  
  ctx.globalCompositeOperation = 'xor';
  ctx.shadowBlur=shadowBlur / 10.0;
  ctx.shadowOffsetX=0;
  ctx.shadowOffsetY=0;
  ctx.shadowColor="blue";
  ctx.lineWidth =1;
  // ctx.stroke();

  shadowBlur += offset;
  if(shadowBlur > 15 || shadowBlur < 1){
    offset *= -1;
  }
}

···

如果做一些叠加绘制,还可以实现如下效果:

   function xor(){
      ctx.clearRect(0,0,canvas.width,canvas.height);
      ctx.globalCompositeOperation = 'xor';
      ctx.shadowBlur= shadowBlur;
      ctx.shadowOffsetX = 0;
      ctx.shadowOffsetY = 0;
      ctx.shadowColor="red";
      ctx.lineCap = "round";
      ctx.lineJoin  = "round";
      ctx.lineWidth = 20;
      ctx.strokeStyle = "red";
      ctx.beginPath();
      ctx.moveTo(300,300);
      ctx.lineTo(750,300);
      ctx.quadraticCurveTo(800,300,800,350);
      ctx.lineTo(800,450);
      ctx.quadraticCurveTo(800,500,750,500);
      ctx.lineTo(300,500);
      ctx.stroke();
      // ctx.stroke();
      
      ctx.globalCompositeOperation = 'destination-out';
      ctx.shadowBlur=shadowBlur / 10.0;
      ctx.shadowOffsetX=0;
      ctx.shadowOffsetY=0;
      ctx.shadowColor="red";
      ctx.lineWidth =5;
      ctx.stroke();

      shadowBlur += offset;
      if(shadowBlur > 15 || shadowBlur < 1){
        offset *= -1;
      }
    }

结语

至此文章已经到达尾声,我们可以总结一下绘制内阴影效果所用到的技术点

  1. CanvasRenderingContext2D.globalCompositeOperation
  2. CanvasRenderingContext2D.shadowBlur

其中globalCompositeOperation是一个有意思的属性,通过设置不同的参数,可以实现很多不同的效果。比如如下的效果就用到了这个属性:
image.png

有兴趣的读者可以关注往期更多的文章。

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

查看原文

赞 0 收藏 0 评论 0

netcy 发布了文章 · 2020-12-29

利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果

利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果

前言

近日公司接到一个轨道系统的需求,需要将地铁线路及列车实时位置展示在大屏上。既然是大屏项目,那视觉效果当然是第一重点,咱们可以先来看看项目完成后的效果图。
line.gif
可以看到中间线路里轨道的效果是非常炫酷的,那么本文的主要内容就是讲解如何在canvas上绘制出这种效果。

分析设计稿

先看看设计稿中的轨道效果
123.jpg
程序员解决问题时经常喜欢用到的方法是把一个大问题拆解为若干个小问题然后逐一处理,也就是分而治之,所以我在思考这个轨道效果的实现时,也是先考虑到将它拆解。
根据设计稿我们可以看到这个线路实际上是由 外层的空心线+发光效果+内层的斑马线+倒影 组成的,所以我们要做的就是如何处理这几个小问题。

实现效果

绘制空心线与发光效果

绘制空心线时我们需要利用到[CanvasRenderingContext2D.globalCompositeOperation](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation)这个属性,详细原理可以查看canvas 绘制双线技巧,本文不再做赘述。
了解实现原理之后动手就很容易了,简述思路就是:
通过ctx.globalCompositeOperation = "destination-out"绘制空心线,再利用canvas的阴影配置来模拟发光的效果。
直接上代码:

//  获取页面里的画布元素和其上下文对象
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
//  由于ctx.globalCompositeOperation = "destination-out"会影响到画布上已有的图像
//  所以需要先创建一个离屏canvas,把空心线绘制到离屏canvas上,再将离屏canvas绘制到页面的画布中
var tempCanvas = document.createElement("canvas");
tempCanvas.width = 800;
tempCanvas.height = 800;
var tempCtx = tempCanvas.getContext("2d");
//  创建坐标点用来连线
var points = [createPoint(50, 50), createPoint(500, 50), createPoint(500, 500)];
//  配置参数
var options = {
  color: "#03a4fe", //  轨道颜色
  lineWidth: 26,    //  总宽度
  borderWidth: 8,   //  边框宽度
  shadowBlur: 20,   //  阴影模糊半径
};
paint(ctx, points, options);
//  绘制
function paint(ctx, points, options) {
  paintHollow(tempCtx, points, options);
  //    将离屏canvas绘制到页面上
  ctx.drawImage(tempCanvas, 0, 0);
}
/**
 * 绘制空心线
 * @param {*} ctx 画布上下文
 * @param {*} points 坐标点的集合
 * @param {*} options 配置 
 */
function paintHollow(
  ctx,
  points,
  { color, lineWidth, borderWidth, shadowBlur }
) {
    //  连线
  paintLine(ctx, points);
  //    添加配置参数
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = color;
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  //    利用阴影
  ctx.shadowColor = color;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = shadowBlur;
  ctx.stroke();
  ctx.globalCompositeOperation = "destination-out";
  ctx.lineWidth -= borderWidth;
  ctx.strokeStyle = color;
  ctx.stroke();
  ctx.globalCompositeOperation = "source-over";
}
/**
 * 根据点位绘制连线
 * @param {*} ctx 画布上下文
 * @param {Array} points 坐标点的集合
 */
function paintLine(ctx, points) {
  var pointIndex = 0,
    p0,
    value,
    pointCount = points.length;
  p0 = points[0];
  ctx.beginPath();
  ctx.moveTo(p0.x, p0.y);
  for (pointIndex = 1; pointIndex < pointCount; pointIndex++) {
    value = points[pointIndex];
    ctx.lineTo(value.x, value.y);
  }
}

效果图
image.png

绘制倒影

可以看到设计稿里的倒影效果就是在轨道下方再次绘制了一条透明度较低的空心线,所以这里实现起来就比较简单了,稍微改造一下paintHollow方法就可以。

/**
 * 绘制空心线
 * @param {*} ctx 画布上下文
 * @param {*} points 坐标点的集合
 * @param {*} options 配置
 * @param {*} isReflect 当前绘制的是否是倒影效果
 */
function paintHollow(
  ctx,
  points,
  { color, lineWidth, borderWidth, shadowBlur, reflectOffset },
  isReflect = false
) {
  if (!isReflect) {
      //    绘制倒影的时候透明度降低
    ctx.globalAlpha = 0.5;
    //  通过自调绘制一个倒影效果出来
    paintHollow(
      ctx,
      points.map(({ x, y }) => {
        return { x, y: y + reflectOffset };
      }),
      { color, lineWidth, borderWidth, shadowBlur: 0 },
      true
    );
    ctx.globalAlpha = 1;
  }
  //  连线
  paintLine(ctx, points);
  //    添加配置参数
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = color;
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  //    利用阴影
  ctx.shadowColor = color;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = shadowBlur;
  ctx.stroke();
  ctx.globalCompositeOperation = "destination-out";
  ctx.lineWidth -= borderWidth;
  ctx.strokeStyle = color;
  ctx.stroke();
  ctx.globalCompositeOperation = "source-over";
}

效果图
image.png

绘制轨道中间的斑马线效果

中间的斑马线效果我们又可以再拆分为两个部分,先绘制一条底色的连线,然后再通过lineDash属性绘制一条虚线,就可以达到设计稿上的效果了。


/**
 * 绘制轨道中间部分
 * @param {*} ctx 
 * @param {*} points 
 * @param {*} param2 
 */
function paintInner(
  ctx,
  points,
  { color, innerWidth, borderWidth, innerColor, shadowBlur }
) {
  ctx.lineCap = "round";
  ctx.lineJoin = "round";
  paintLine(ctx, points);
  ctx.lineWidth = innerWidth;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = shadowBlur;
  ctx.strokeStyle = innerColor;
  ctx.shadowColor = color;
  //  先根据中间部分的颜色绘制一条线出来
  ctx.stroke();
  ctx.lineCap = "butt";
  ctx.setLineDash([5, 15]);
  ctx.lineDashOffset = 0;
  const { r, g: green, b } = getRgba(color);
  //  再根据轨道的主色调绘制一条透明度较低的虚线
  ctx.strokeStyle = `rgba(${r},${green},${b},0.4)`;
  ctx.stroke();
}
/**
 * 获取一个颜色值的r,g,b,a
 * @param {*} color 
 */
function getRgba(color) {
  if (!canvas1 || !ctx1) {
    canvas1 = document.createElement("canvas");
    canvas1.width = 1;
    canvas1.height = 1;
    ctx1 = canvas1.getContext("2d");
  }
  canvas1.width = 1;
  ctx1.fillStyle = color;
  ctx1.fillRect(0, 0, 1, 1);
  const colorData = ctx1.getImageData(0, 0, 1, 1).data;
  return {
    r: colorData[0],
    g: colorData[1],
    b: colorData[2],
    a: colorData[3],
  };
}

效果图
image.png
至此我们就还原了设计稿上的轨道效果了!

结语

至此文章已经到达尾声,我们可以总结一下绘制这条轨道线路效果所用到的技术点

  1. CanvasRenderingContext2D.globalCompositeOperation
  2. CanvasRenderingContext2D.shadowBlur
  3. CanvasRenderingContext2D.setLineDash()
  4. 离屏canvas技巧

可以看到想要达到好的效果还是不容易的,需要我们灵活配合使用多种绘制技巧,希望这篇文章能对大家有所帮助!

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

查看原文

赞 8 收藏 5 评论 0

netcy 发布了文章 · 2020-12-29

canvas可视化效果之内阴影效果

canvas可视化效果之内阴影效果

楔子

在之前的一个轨道交通可视化项目中,运用到了很多绘制技巧。 可以参考 之前的一篇文章 《利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果

效果图中的轨道,就同时存在外发光和内发光效果的效果。

外发光效果

我们知道外发光效果是很容易实现的,直接通过设置阴影效果即可达到。比如我们随便绘制一条线段,加上阴影效果,看起来就是外发光的效果:

      ctx.clearRect(0,0,canvas.width,canvas.height);
      ctx.shadowBlur= 20;
      ctx.shadowOffsetX = 0;
      ctx.shadowOffsetY = 0;
      ctx.shadowColor="red";
      ctx.lineCap = "round";
      ctx.lineJoin  = "round";
      ctx.lineWidth = 10;
      ctx.strokeStyle = "blue";
      ctx.beginPath();
      ctx.moveTo(300,300);
      ctx.lineTo(750,300);
      ctx.quadraticCurveTo(800,300,800,350);
      ctx.lineTo(800,450);
      ctx.quadraticCurveTo(800,500,750,500);
      ctx.lineTo(300,500);
      ctx.stroke();

效果图如下:

如果绘制圆形效果如下:

上面的代码都容易理解,就是通过shadowBlur产生渐变阴影的效果。 默认的阴影,我们称之为外阴影,意思都是图像向往展开的阴影效果。

内阴影

接下来的问题可能就变得有点难度。如果我们需要如下的一个内阴影的效果呢?

有人说,简单,一个渐变就搞定了。 那再看看下面这个图像呢?

还是没问题,还是可以通过渐变来搞定,只是渐变的stop设置要麻烦一点罢了。 如果在复杂一些的图形呢,比如下面的线段效果:

对于上面的线段的内阴影效果,就很难使用简单的渐变来实现了。

如何绘制内阴影效果

要实现上面的内阴影效果,首先还是使用shadowBlur参数,然后把ctx的globalCompositeOperation参数设置为“source-out” 即可。 试试如下代码:

 ctx.globalCompositeOperation = 'source-out';
     ctx.beginPath();
     ctx.beginPath();
    ctx.moveTo(300,300);
    ctx.lineTo(750,300);
    ctx.quadraticCurveTo(800,300,800,350);
    ctx.lineTo(800,450);
    ctx.quadraticCurveTo(800,500,750,500);
    ctx.lineTo(300,500);
    ctx.lineCap = "round";
     ctx.shadowBlur =15;
     ctx.lineWidth = 20;
     ctx.shadowColor="blue";
     ctx.fillStyle = 'red';
     ctx.strokeStyle = 'red';
     ctx.stroke();

最终绘制的效果就是上面的线段图的效果:

同时绘制内外阴影效果

如果修改globalCompositeOperation为“xor”,我们还可以得到既有内阴影又有外阴影的效果。 代码如下:

 ctx.globalCompositeOperation = 'xor';
     ctx.beginPath();
     ctx.beginPath();
    ctx.moveTo(300,300);
    ctx.lineTo(750,300);
    ctx.quadraticCurveTo(800,300,800,350);
    ctx.lineTo(800,450);
    ctx.quadraticCurveTo(800,500,750,500);
    ctx.lineTo(300,500);
    ctx.lineCap = "round";
     ctx.shadowBlur =15;
     ctx.lineWidth = 20;
     ctx.shadowColor="red";
     ctx.fillStyle = 'red';
     ctx.strokeStyle = 'red';
     ctx.stroke();

绘制的效果如下:

内阴影的缺陷

上述方法实现的内阴影颜色的颜色只能和绘制主体一样的颜色,而不能像外阴影的颜色一样,可以自由定义。 比如把上述代码中的shadowColor改成blue,只有外阴影的颜色改变了:

 ctx.globalCompositeOperation = 'xor';
     ctx.beginPath();
     ctx.beginPath();
    ctx.moveTo(300,300);
    ctx.lineTo(750,300);
    ctx.quadraticCurveTo(800,300,800,350);
    ctx.lineTo(800,450);
    ctx.quadraticCurveTo(800,500,750,500);
    ctx.lineTo(300,500);
    ctx.lineCap = "round";
     ctx.shadowBlur =15;
     ctx.lineWidth = 20;
     ctx.shadowColor="red";
     ctx.fillStyle = 'red';
     ctx.strokeStyle = 'red';
     ctx.stroke();

最终的效果如下图所示:

从图上可以看出只有外阴影颜色改变了,内阴影使用的本体的颜色。

实现闪烁的效果

基于上面的实现,我们可以实现一个阴影闪烁的效果,只需要不断更改shadowBlur的值,代码如下:
···
setInterval(()=>{
xor();
},10)

let shadowBlur = 5;
let offset = 0.5;



function xor(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.globalCompositeOperation = 'xor';
  ctx.shadowBlur= shadowBlur;
  ctx.shadowOffsetX = 0;
  ctx.shadowOffsetY = 0;
  ctx.shadowColor="red";
  ctx.lineCap = "round";
  ctx.lineJoin  = "round";
  ctx.lineWidth = 10;
  ctx.strokeStyle = "blue";
  ctx.beginPath();
  ctx.moveTo(300,300);
  ctx.lineTo(750,300);
  ctx.quadraticCurveTo(800,300,800,350);
  ctx.lineTo(800,450);
  ctx.quadraticCurveTo(800,500,750,500);
  ctx.lineTo(300,500);
  ctx.stroke();
  // ctx.stroke();
  
  ctx.globalCompositeOperation = 'xor';
  ctx.shadowBlur=shadowBlur / 10.0;
  ctx.shadowOffsetX=0;
  ctx.shadowOffsetY=0;
  ctx.shadowColor="blue";
  ctx.lineWidth =1;
  // ctx.stroke();

  shadowBlur += offset;
  if(shadowBlur > 15 || shadowBlur < 1){
    offset *= -1;
  }
}

···

如果做一些叠加绘制,还可以实现如下效果:

   function xor(){
      ctx.clearRect(0,0,canvas.width,canvas.height);
      ctx.globalCompositeOperation = 'xor';
      ctx.shadowBlur= shadowBlur;
      ctx.shadowOffsetX = 0;
      ctx.shadowOffsetY = 0;
      ctx.shadowColor="red";
      ctx.lineCap = "round";
      ctx.lineJoin  = "round";
      ctx.lineWidth = 20;
      ctx.strokeStyle = "red";
      ctx.beginPath();
      ctx.moveTo(300,300);
      ctx.lineTo(750,300);
      ctx.quadraticCurveTo(800,300,800,350);
      ctx.lineTo(800,450);
      ctx.quadraticCurveTo(800,500,750,500);
      ctx.lineTo(300,500);
      ctx.stroke();
      // ctx.stroke();
      
      ctx.globalCompositeOperation = 'destination-out';
      ctx.shadowBlur=shadowBlur / 10.0;
      ctx.shadowOffsetX=0;
      ctx.shadowOffsetY=0;
      ctx.shadowColor="red";
      ctx.lineWidth =5;
      ctx.stroke();

      shadowBlur += offset;
      if(shadowBlur > 15 || shadowBlur < 1){
        offset *= -1;
      }
    }

结语

至此文章已经到达尾声,我们可以总结一下绘制内阴影效果所用到的技术点

  1. CanvasRenderingContext2D.globalCompositeOperation
  2. CanvasRenderingContext2D.shadowBlur

其中globalCompositeOperation是一个有意思的属性,通过设置不同的参数,可以实现很多不同的效果。比如如下的效果就用到了这个属性:
image.png

有兴趣的读者可以关注往期更多的文章。

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

查看原文

赞 0 收藏 0 评论 0

netcy 发布了文章 · 2020-12-29

webgl智慧楼宇发光系列之线性采样下高斯模糊

[toc]

webgl智慧楼宇发光系列之线性采样下高斯模糊

前面一篇文章 <webgl智慧楼宇发光效果算法系列之高斯模糊>,   我们知道了 高斯模糊的本质原理,就是对每个像素,按照正态分布的权重去获取周边像素的值进行平均,是一种卷积操作。

同时我们可以指定周边像素的数量,比如可以是3X3,或者5X5,通用的表达就是N X N, 数字N通常称之为模糊半径,这在之前的文章的代码中有体现(uRadius):

uniform float uRadius;
float gaussianPdf(in float x, in float sigma) {
  return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
}
void main() {
  for( int i = 1; i < MAX_KERNEL_RADIUS; i ++ ) {
    float x = float(i);
    if(x > radius){
      break;
    }
    ...
  }
  vec4 result = vec4(1.0) - exp(-diffuseSum/weightSum * uExposure);
  gl_FragColor = result;
}
`

效率问题

通常,我们希望模糊的效果越强烈,模糊半径就会要求越大。所谓的半径就是上面的数字N。
我们知道,要实现一个NxN大小的高斯模糊,在纹理的每个像素点,都需要去获取周边N个像素点。因为1024_1024大小的纹理,要实现33 33 大小的高斯模糊,需要访问大概1024 1024 _ 33 * 33≈11.4亿个纹理像素,才能应用整个图像的模糊效果。

为了获得更有效的算法,我们来看看高斯函数的一些特性:

  • 二维高斯函数可以通过将两个一维高斯函数相加来计算。
  • 分布为2σ的高斯函数等于分布为σ的两个高斯函数的乘积。

高斯函数的这两个属性为我们提供了进行大量优化的空间。

基于第一个属性,我们可以将二维高斯函数分成两个一维函数。在使用片段着色器的情况下,我们可以将高斯滤镜分为水平模糊滤镜和垂直模糊滤镜,在渲染后仍可获得准确的结果。 这个时候,1024_1024大小的纹理,要实现33 33 大小的高斯模糊,需要访问大概1024 1024 _ 33*2≈6,900万个纹理提取。这种优化明细减少了一个量级。文章 《webgl智慧楼宇发光效果算法系列之高斯模糊》已经实现了这一优化。

第二个属性可用于绕过平台上的硬件限制,这些平台仅在一次pass中仅支持有限数量的纹理提取。

线性采样

到此,我们知道了把一个二维的高斯模糊 分离成两个一维的高斯模糊。效率上也有了大幅度的提高。但是实际上,我们还可以通过线性采样的特性进一步提高效率。

我们知道,要获取一个像素信息,就要做一次贴图的读取。这就意味33个像素信息,就需要做33次贴图的读取操作。 但是由于在GPU上面可以随意进行双线线性插值,而没有额外的性能消耗。  这就意味着,如果我们不再像素的中心点读取贴图,就可以获得多个像素的信息。  如下图所示:

假设两个像素,我们在像素1中心点读取贴图就是获取像素1的颜色,在像素2中心点读取贴图就是获取像素2的颜色;而在像素1中心点和像素2中心点的某个位置读取贴图,则会获取像素1和像素2的颜色的加权平均的效果。

因为我们做高斯模糊的时候,本身就是获取周边相邻元素的加权平均值,因此利用线性采样的这个特性,可以把原本2个像素的采样,减少为一次采样。 如果原本33次采样,则可以减少到17次。

对于两个纹素的采样,需要调整坐标使其与纹素#1中心的距离等于纹素#2的权重除以两个权重之和。同样的,坐标与纹素#2中心的距离应该等于纹素#1的权重除以两个权重之和。

然后我们就有了计算线性采样高斯滤波的权重和位移公式:

代码讲解

  • 首先定义一个uniform变量,该变量表示是否启用线性采样的方法:
uniform bool uUseLinear;
  • 然后如果使用线性采样,就把原本的采样次数减少一半:
 if(uUseLinear){
    radius = uRadius / 2.0;
  }
  • 再然后,如果使用线性采样,就使用上述的公式进行像素提取:
if(uUseLinear){
      // http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
      float t1 = 2.0 * x - 1.0,t2 = 2.0 * x ;
      float w1 = gaussianPdf(t1,fSigma);
      float w2 = gaussianPdf(t2,fSigma);
      w = w1 + w2;
      t = (t1 * w1 + t2 * w2) / w;
    }

    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;

最终的绘制效果如下:

其中左边的未使用线性采样的机制,而右边的使用了线性采样,可以看出右边再减少了一半的采样的情况下,效果和左边的基本没有差别。

而效率上,通过测试,右边比左边大概提高了40%的渲染效率。

总结

通过线性采样的机制,我们可以看到效率提高了近一倍。这在一些对性能要求高得场景或者移动终端是很有意义。

其实要做出一个好的发光效果,涉及到相关算法是很多了,而且细节之处都需要关注。

先看看我们已经做了得一些发光楼宇得案例吧, 以下都是再简单模型(立方体) + 贴图 + 光照 + 发光 出来得效果,如果模型层面在优化,应该还可以有更酷效果:

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

参考文档

参考文档:http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
本文部分素材使用了参考文档中的内容。

查看原文

赞 0 收藏 0 评论 0

netcy 发布了文章 · 2020-10-14

大屏/拓扑/三维编辑器的设计与思考

从去年年初开始,我们团队内部就在做两个编辑器:

  • 3d编辑器 支持搭建3d场景。
  • 拓扑/大屏编辑器 支持搭建大屏/拓扑/组态场景。

开发编辑器的主要目是为了提高团队内部项目的交付效率,目前 两个编辑器都引用到团队的相关项目中。当然,编辑器目前也支持和其他公司的项目合作。 下面是几个项目的示例图:
示例图

示例图2

示例图3

示例图4

中间是一个三维场景,使用三维编辑器编辑而成,周边是一些大屏元素,使用大屏编辑器编辑。

和一般的大屏编辑器不同,我们的大屏编辑器不仅支持大屏元素的排布,也支持分层拓扑图,架构图,交通图等,以及组态功能。 基于canvas + HTML5元素共同开发,能够支持更多丰富的功能和效果。 比如下面的拓扑效果:

拓扑图

架构图

还有轨道交通,地铁,交通线路图等:
交通线路

当然还有组态图,比如下面的电力一次接线图等。
接线图

在编辑器的开发过程中,经历了很多次的迭代,自我否定,然后是不断的成长。 产品的开发过程中,我们需要对产品的设计进行深入的探讨。 下面是一些感悟,和大家分享。本文主要是分享产品功能设计层面,而非软件开发架构。

易用性

易用性是一个很宽泛的概念。但其确实是一个产品设计中,最重要的一个因素,甚至可以说,易用性决定了产品的成败。 产品是帮助用户来达到某个任务的,因此首先产品需要有可用性,可用性就是通过产品某项功能,能够达到逾期的目的。比如用户需要可以配置一个chart图表,而在编辑器中有chart图表配置的功能。产品的可用性就是要确保该产品能够正常的完成工作,这是最基本的要求。

但是光有可用性是远远不够的,要想一个产品能够得到认可,一定要在易用性上面下狠功夫。易用性是指最终给到用户使用的产品是否容易使用,通常情况下,易用性越差,用户越难以接受一款产品。

我们的产品在易用性方面也是做了很多努力的。比如编辑器中提供了一些常用的复制功能,能否方便使用者提高效率和精确度:

  • 各种对齐功能
  • 平均分布
  • 等距排布
  • 网格,自动吸附
  • 批量复制
  • 拖拽复制
  • 组合与打散
  • 锁定
  • undo redo
  • 。。。

以上功能,都是为了能够让使用者在使用的过程中更加容易。
易用性辅助按钮

要做好易用性的工作,除了一些常用的辅助性工作外,还需要遵从一些其他的规则。比如

  • 直观性 有时候,会不自觉的实现很多隐藏的功能,比如使用快捷键或者右键菜单。 使用快捷键或者右键菜单要谨慎,比较忌讳的是一个功能,只有快捷键才能调出,而没有直观的按钮。并且快捷键没法快速查看,传递给用户的设计就是更为糟糕的。当然快捷键在有直观传递的情况下,有时候是可以增加用户使用效率的。
  • 明确性,避免模糊性和歧义性 功能点的设计应该明确,不应该表达不清.
  • 简单性 每个功能点 应该设计的相对简单,不应该把多个功能通过逻辑开关的方式整合在一起。

当然还有更多的易用性,后面涉及到的在做介绍。

通用性

有时候在遇到一些需求的时候,我们需要把具体的需求抽象成更抽象的需求; 把特 殊的需求,提高成更加通用的需求。看似是把把简 单的需求复杂化了,但确实很有必要,因为其可以保持软件本身不会变的越来越臃肿。

比如曾经有这样的一个组件需求:
组件

类似于进度条的需求。 要说实现这个组件本身,难度并不大,使用canvas的绘制,重复绘制很多矩形,通过数据来定义矩形重复的数量, 即可达到。

然而,其实可以从中间提前一个更加通用的规则,就是某个对象的重复绘制。 在此,我们把具体的矩形 抽象成为更为抽象的某个对象。 而某个对象如何定义呢? 其实很简单,可以把我们现有的图元库中的所有图元作为“某个对象”,那么最终的需求就变成:

  • 对于图元库中的所有图元,可以进行重复绘制,重复的次数通过数据来定义。

于是我们最终的定义了个RepeatNode的对象(重复节点的意思),RepeatNode可以指定一个图元进行重复,如下图所示:

重复节点

该节点对象,可以指定要重复的对象(前面红色框的矩形对象就是),可以指定重复此处,间距等等属性。 当如重复次数还可以通过动态数据来驱动,以此达到动态的效果。

有了repeatnode,我们除了实现原始的需求效果外,还可以轻松的实现其他类似的效果,比如:
更多示例

后续还有个类似HTML5中tab标签的功能需求,要求能够点击按钮进行页面切换:
tab标签

编辑器前期是使用事件派发的机制来实现的,比如上面,点击保障措施,就脚本派发一个事件,然后相关的元素展示出来,而不相关的元素隐藏起来。

通过派发事件和接受事件来展示和隐藏内容,效果是可以的,问题在于易用性并不好,通过编辑器实现起来复杂,一般的实施人员用起来难度大。 因此,可以考虑抽象出一个TabNode,有技术人员提出疑问是,tabNode的标题部分并不固定,可能各种变化。但是实际上,我们可以不按照普通的Tab的思路来做。 TabNode只是要给一个Tab容器,该容器可以配置指定哪些元素作为tab标签的标题(这个和前面的RepeatNode类似),这样就解决了TabNode的标题可能各种变化的问题。

实际上,还是可以复用原本的事件派发的机制来实现TabNode的切换。

灵活性和易用性

灵活性和易用性有时候是相互矛盾的。 比如前面说的TabNode的问题,用事件派发的脚本机制,肯定是最灵活的,因为脚本的机制可以实现几乎所有的需求变化,然而易用性却不好了。

既要灵活性,也有易用性。应该如何做呢? 我比较认同的就是多层次的设计思路,最底层的是比较灵活的实现方式; 在灵活的底层的基础上,我们在进一步的封装,把常用的功能和元素封装得更加易用。

比如前面TabNode就是这个思路。

还有编辑器支持echart图表和HtmlNode功能。 echart的配置千变万化,如果把所有得配置项都抽出来,做成可视化得模式,工作量巨大,因此编辑直接提供了json配置,就很灵活,但是易用性差;而HtmlNode类似于Vue的模板机制,也是需要比较专业开发人员才能处理。

echart图表和HtmlNode都是灵活性很好得元素,但是易用性差。 那么怎么解决呢?我们得思路是提供一个模板功能,把常用的项目上面做得好得配置,做成模板,放到模板库里面。 后续非专业人员,可以从模板库中去选择,而不是手动去配置json或者html标签。

可扩展性

编辑器中最常见得变化就是图元得变化,因此我们提供了图元编辑器。通过图元编辑器可以编辑出各种需要的图元,同时也支持SVG图像的导入处理等,比如如下图项目中用到的图元就是:

扩展图元

上述的图元都有动态的效果,比如第一个三段式的,每段长度是可以变化的。 第二个数量可变,第三个数量和高度可变。

上面只是举例说明,实际的编辑的图元非常多,也很灵活:
编辑的图元

同时,还提供了插件机制,可以适当的扩展原本的功能,便于实现项目的一些特殊的需求。

总结

上面是一些设计与思考,主要是基于拓扑/大屏编辑器的思考。为了有更好的产品,我们一直在努力的架构,设计与思考。

有关三维编辑器,有很多类似的设计理念,以后又机会再说。 如果大家有好的想法,可以邮件或者微信作者,一起讨论:
Email: terry.tan@servasoft.com, 微信:541002349。 也欢迎关注公众号 "ITMan彪叔" 接受更多消息。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 51 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Canvas高级进阶

    html5 canvas的入门很简单,基础API数量也就10几个。但是要想通过canvas实现很多酷炫的效果,却涉及到很多图形学知识、算法知识、数学知识,以及实际的经验和技巧。 本专栏将是canvas高级内容的集大成者。 本专栏主要结合笔者很多年使用canvas的实操经验,倾情打造重量级的canvas进阶指南。

  • webgl入门到高级进阶

    相比canvas 2d功能,webgl的底层API显得比较晦涩难懂。本专栏希望通过通俗易懂的语言,让大家快速入门webgl;而在本专栏的后面部分,还会介绍一些较为高级的内容,让读者可以快速成长为webgl高手。

注册于 2016-08-17
个人主页被 1.9k 人浏览