前言

在当代社会,故宫已经成为一个具有多元意义的文化符号,在历史、艺术、文化等不同领域发挥着重要的作用,在国际上也成为能够代表中国文化甚至中国形象的国际符号。近几年故宫的观众接待量逐年递增,年接待量已突破千万,根据故宫的文物特点与开放模式,必须及时建立一套完整的集监控与防患应急于一体的现代化监控系统。

故宫人流量动态监控系统采用 Hightopo 的  HT for Web  产品来构造 故宫 3D 动态可视化场景,通过将现场部署的传感器、监控设备等装置与智能联网设备集成到互联网上,对故宫当前的人流状态、人流拥挤度进行实时监测,并生成人流量热力图直观的展示现场人流数据,以预防拥挤、踩踏等意外事故的发生。

预览地址:故宫人流量动态监控系统

整体预览图:

全景图预览:

代码实现

创建场景

项目目录结构如下:

index.js 是 src 下的入口文件,创建了一个 由 main.js 中导出的 Main 类,Main 类中创建 3D 组件和 2D 组件,利用 g2d.deserialize() 方法将 json 矢量背景图反序列化显示在 2D 组件上并利用 this.load() 方法进行 3D 场景的加载工作,在 Main 类中使用了 HT 自带的事件派发器,this.event.fire() 和 this.event.add() 分别是派发事件和订阅事件,在本示例中通过事件订阅与派发完成3D场景的切换效果,关键代码如下:

  1. import util from '../util/util';
  2. import forbiddenCity from './forbiddenCity.js'
  3. import heatMap from './heatMap.js'
  4. import loadScene from './loadScene.js'
  5. class Main {
  6. constructor() {
  7. let g3d = (this.g3d = new ht.graph3d.Graph3dView());
  8. this.g3dDm = this.g3d.dm();
  9. let g2d = (this.g2d = new ht.graph.GraphView());
  10. this.g2dDm = this.g2d.dm();
  11. //将 3D 组件加入到 body 下
  12. g3d.addToDOM();
  13. // 将 2D 组件加入到 3D 组件的根 div 下,父子 DOM 事件会冒泡,这样不会影响 3D 场景的交互
  14. g2d.addToDOM(g3d.getView());
  15. // 初始化场景
  16. this.init();
  17. }
  18. init() {
  19. // 2D面板加载
  20. this.g2d.deserialize('displays/htproject_2019_q4/故宫/首页.json', (json, dm, g2d, datas) => {
           
  21. });
  22. this.forbiddenCity = new forbiddenCity(this);
  23. this.heatMap = new heatMap(this);
  24. // 首页3D场景加载
  25. this.load(this.forbiddenCity);
  26. // 订阅事件
  27. this.addListener(e => {
  28. if (e.type === 'loadforbiddenCity') {
  29. this.load(this.forbiddenCity);
  30. } else if (e.type === 'loadheatMap') {
  31. this.load(this.heatMap);
  32. }
  33. });
  34. }
  35. load(scene) {
  36. let old = this.activeScene;
  37. if (old) {
  38. old.tearDown();
  39. }
  40. this.activeScene = scene;
  41. scene.setUp();
  42. }
  43. fire(e) {
  44. this.event.fire(e);
  45. }
  46. addListener(cb, scope) {
  47. this.event.add(cb, scope);
  48. }
  49.  
  50. }
  51. export default Main;

由上可以看出在 Main 类中我们通过订阅事件提供了场景切换的代码,即通过调用两个场景文件中的 setUp() 方法来完成 3D 场景的切换让我们来看下在 forbiddenCity.js 与 heatMap.js 中是如何进行场景切换的:

  1. setUp() {
  2. let g3d = this,
  3. dm3d = g3d.dm();
  4. super.setUp();
  5. util.setSceneLevel('forbiddenCity');
  6. // 清空数据容器
  7. dm3d.clear();
  8. // 反序列化 3D 图纸
  9. g3d.deserialize('scenes/htdesign/city/故宫/故宫.json', (json, dm, g3d, datas) => {
  10.  
  11. });
  12. }
  1. setUp() {
  2. let g3d = this,
  3. dm3d = g3d.dm();
  4. super.setUp();
  5. util.setSceneLevel('heatMap');
  6. // 清空数据容器
  7. dm3d.clear();
  8. // 反序列化 3D 图纸
  9. g3d.deserialize('scenes/htdesign/city/故宫/热力图.json', (json, dm, g3d, datas) => {
  10.  
  11. });
  12. }

以上代码可以看出我们在每次切换场景时都会调用数据容器的 clear() 方法来清空数据然后再调用 g3d.deserialize() 方法反序列化加载新场景图纸,从而完成新旧场景的加载和清空。

投影实现

为增强 3D 场景的立体感,在最新版本的 HT 核心包中新增了场景投影效果配置函数,用户通过调用 enableShadow() 和 disableShadow() 方法可以实现开启关闭 3D 投影效果,此外还可以通过设置 node.s('shadow.cast', false) 对部分不需要投影的模型进行投影关闭处理,投影关键代码:

  1. import util from '../util/util';
  2. const loadScene = {
  3. shadow(g3d) {
  4. var ssc = function(filter) {
  5. var nodes = g3d.dm().toDatas(filter);
  6. if (!nodes.length) {
  7. return;
  8. };
  9. nodes.each(function(node) {
  10. node.s('shadow.cast', false);
  11. });
  12. }
  13. var nameFilter = function(name) {
  14. return function(node) {
  15. return node.getDisplayName() === name;
  16. }
  17. }
  18. var typeFilter = function(type) {
  19. return function(node) {
  20. return node.s('shape3d') === type;
  21. }
  22. }
  23. ssc(nameFilter('路线'));
  24. ssc(nameFilter('布景'));
  25. ssc(nameFilter('灯光'));
  26. ssc(typeFilter('models/医疗/阴影_1.json'));
  27. ssc(typeFilter('models/医疗/地面.json'));
  28. ssc(typeFilter('models/htdesign/Identification/point/riangle_01.json'))
  29. // 为了编组用的 box
  30. ssc(typeFilter('box'));
  31. if (util.getSceneLevel() === 'forbiddenCity') {
  32. g3d.enableShadow({
  33. // 投影 x 轴角度
  34. degreeX: 55,
  35. // 投影 z 轴角度
  36. degreeZ: -35,
  37. // low / medium / high / ultra / 4096数值
  38. quality: 4096,
  39. // 深度浮点偏差补足
  40. bias: -0.0003,
  41. // none / hard / soft
  42. type: 'soft',
  43. // type 为 hard / soft 时,补充的边缘厚度,用来提供更柔和的边缘
  44. radius: 1.0,
  45. // 阴影强度, 1 为黑色
  46. intensity: 0.45
  47. });
  48. g3d.iv();
  49. }
  50.  
  51. }
  52. }
  53. export default loadScene

动画实现

飞鸟动画

飞鸟动画可以拆分为两个步骤:1.飞鸟沿固定路线环绕故宫的飞行动作以及上下位置变化动作,2.飞鸟自身的翅膀扇动动作。我们使用 HT 自带的 ht.Default.startAnim 函数让飞鸟模型沿着三维空间管道做周期运动,在动画中定义了一个变量 count 每次动画都递增,通过 Math.cos(count % 36 * 10 * Math.PI / 180) 函数使值在 1 和 -1 之间做周期变化,配合 setRotationZ() 方法改变翅膀在 3D 拓扑中沿 z 轴的旋转角度从而达到飞鸟翅膀上下扇动,关键代码如下:

  1. // 飞鸟动画
  2. flyerAnim(g3d) {
  3. const dm3d = g3d.dm();
  4. let polyline = dm3d.getDataByTag('polyline');
  5. let flyers = dm3d.getDataByTag('flyers');
  6. let count = 0;
  7. let radomArr = [this.random(20, 80),
  8. this.random(30, 100),
  9. this.random(10, 60),
  10. this.random(10, 50),
  11. this.random(5, 20),
  12. this.random(20, 70)
  13. ];
  14. if (polyline) {
  15.  
  16. let anim = {
  17. // 动画周期毫秒数
  18. duration: 40000,
  19. easing: function(t) {
  20. return t;
  21. },
  22. action: (v, t) => {
  23. if (util.getSceneLevel() !== 'heatMap' && polyline) {
  24. let length = g3d.getLineLength(polyline);
  25. // 获取三维空间管道坐标
  26. if (length) {
  27. let offset = g3d.getLineOffset(polyline, length * v),
  28. point = offset.point,
  29. tangent = offset.tangent,
  30. px = point.x,
  31. py = point.y,
  32. pz = point.z,
  33. tx = tangent.x,
  34. ty = tangent.y,
  35. tz = tangent.z;
  36. flyers.eachChild((bird, index) => {
  37. let ty = bird.getTag().split('_')[1];
  38. let positionZ = pz + index * 50 + radomArr[index] / 3,
  39. positionX = px + (index - 3) * 50 + radomArr[index] / 3,
  40. positionY = py + radomArr[index] / 5;
  41. if (index > 2) positionZ = pz - (index - 6) * 50 + radomArr[index] / 3;
  42. // 设置飞鸟翅膀扇动动画
  43. const pos = count + index,
  44. pos2 = count - index * 6;
  45. if (pos2 > 0) {
  46. if (!bird._posId) bird._posId = pos2;
  47. bird._posId++;
  48. if (bird._posId > index * 100 + 500 && bird._posId < index * 100 + 600) {
  49. bird.eachChild((child) => {
  50. if (child.getTag() === 'wingLeft') {
  51. child.setRotationZ(0);
  52. } else if (child.getTag() === 'wingRight') {
  53. child.setRotationZ(0);
  54. }
  55. });
  56. if (bird._posId === index * 100 + 599) bird._posId = 1;
  57. } else {
  58. bird.eachChild((child) => {
  59. if (child.getTag() === 'wingLeft') {
  60. child.setRotationZ(child.r3()[2] + Math.cos(bird._posId % 36 * 10 * Math.PI / 180) * 4 * 0.03);
  61. } else if (child.getTag() === 'wingRight') {
  62. child.setRotationZ(child.r3()[2] - Math.cos(bird._posId % 36 * 10 * Math.PI / 180) * 4 * 0.03);
  63. }
  64. });
  65. }
  66. }
  67. // 设置飞鸟飞行轨道动画
  68. bird.p3(positionX + radomArr[index] * v, positionY + radomArr[index] * v + Math.cos(count % 36 * 10 * Math
  69. .PI / 180) * ty * 5, positionZ + radomArr[index] *
  70. v);
  71. // 设置飞鸟朝向位置
  72. bird.lookAt([positionX + radomArr[index] * v + tx, positionY + ty + radomArr[index] * v, positionZ +
  73. radomArr[index] * v + tz
  74. ]);
  75. })
  76. count++;
  77. }
  78.  
  79. }
  80. },
  81. finishFunc: function() {
  82. // 继续执行飞鸟管道动画
  83. this.birdAnim = ht.Default.startAnim(anim);
  84. }
  85. };
  86. if (util.getSceneLevel() === 'forbiddenCity') {
  87. // 执行飞鸟管道动画
  88. this.birdAnim = ht.Default.startAnim(anim);
  89. }
  90. }
  91. }

鸟瞰漫游动画

在飞鸟动画实现的前提下,接下来我们可以进一步以飞鸟模型为中心来生成鸟瞰漫游动画。首先使用 ht.Default.startAnim 函数实时调用飞鸟所在位置,通过 setEye() 和 setCenter() 方法动态设置场景的中心点和相机位置,以此达到从飞鸟的视角俯瞰整个故宫场景的动画效果。关键代码如下:

  1. // 鸟瞰漫游动画
  2. roamingAnim() {
  3. const g3d = this.g3d;
  4. let flyers = g3d.dm().getDataByTag('flyers');
  5. let anim = {
  6. duration: 60000, // 动画周期毫秒数
  7. easing: function (t) {
  8. return t * t;
  9. },
  10. action: function (v, t) {
  11. let flyersP = flyers.p3();
  12. let px = flyersP[0];
  13. let py = flyersP[1];
  14. let pz = flyersP[2];
  15. g3d.setEye(px, py + 50, pz - 400);
  16. g3d.setCenter(px, py, pz);
  17. }
  18. }
  19. this.roaming = ht.Default.startAnim(anim);
  20. }

景深动画

在 HT for Web 中为 3D 组件提供了 enablePostProcessing() 方法,使用者可以通过调用该方法手动开启 3D 场景的景深模糊效果,另外还可以通过设置 aperture 属性改变景深模糊度,在本示例中通过动态改变 aperture 属性形成淡入淡出效果以减少场景切换时的突兀感,关键代码如下:

  1. // 景深动画
  2. depthAnim(g3d, x = 0) {
  3. let dof = g3d.getPostProcessingModule('Dof');
  4. // 景深开启
  5. g3d.enablePostProcessing('Dof', true);
  6. return new Promise((resolve, reject) => {
  7. let anim = {
  8. duration: 1000,
  9. easing: (t) => {
  10. return t * t;
  11. },
  12. action: (v, t) => {
  13. // 动态设置景深阈值
  14. dof.aperture = x - v * 0.02
  15. if (v == 1) resolve('end');
  16. }
  17. }
  18. ht.Default.startAnim(anim);
  19. })
  20. }

主要功能

人流量热力图

热力图以特殊高亮的形式显示游客所在的地理区域的图示,可以非常直观的展示人流量密度信息。本示例中使用 HT 封装的 ht.thermodynamic.Thermodynamic3d() 方法动态生成热力图,关键代码如下:

  1. createHeatMap(heatMapName, num) {
  2. const g3d = this.g3d;
  3. const dm3d = g3d.dm();
  4. let room = dm3d.getDataByTag(heatMapName);
  5. // 获取要生成热力图的矩形区域
  6. let heatRect = room.getRect();
  7. let Vector3 = ht.Math.Vector3;
  8. let tall = 30
  9. let {
  10. x,
  11. y,
  12. width,
  13. height
  14. } = heatRect;
  15. if (width === 0 || height === 0) return
  16. let templateList = [];
  17. // 在热力图区域随机生成 num 个热力点位
  18. for (let index = 0; index < num; index++) {
  19. templateList.push({
  20. position: {
  21. x: this.random(0, heatRect.width),
  22. y: this.random(0, heatRect.height),
  23. z: tall
  24. },
  25. temperature: {
  26. value: 30 + this.random(0, 20),
  27. radius: 90
  28. },
  29. })
  30. }
  31. // 热力图初始化
  32. let thd = window.thd = new ht.thermodynamic.Thermodynamic3d(g3d, {
  33. box: new Vector3(width, height, tall),
  34. min: 15,
  35. max: 55,
  36. interval: 200,
  37. remainMax: false,
  38. opacity: 0.1,
  39. colorStopFn: function (v, step) {
  40. return v * step * step
  41. },
  42. gradient: {
  43. 0: 'rgba(0,162,255,0.14)',
  44. 0.2: 'rgba(48,255,183,0.3)',
  45. 0.4: 'rgba(255,245,48,0.5)',
  46. 0.6: 'rgba(255,73,18,0.9)',
  47. 0.8: 'rgba(217,22,0,0.95)',
  48. 1: 'rgb(179,0,0)',
  49. }
  50. });
  51. thd.setData(templateList);
  52. // 创建热力图
  53. let node = thd.createThermodynamicNode(2, 2, 50);
  54. node.setAnchorElevation(0);
  55. node.setTag('test');
  56. node.p3(room.p3());
  57. node.s({
  58. '3d.selectable': false,
  59. '3d.movable': false,
  60. 'wf.visible': false,
  61. 'shape3d.transparent': true,
  62. });
  63. dm3d.add(node);
  64. }

这里简单的描述下热力图生成步骤:1.首先确定热力图生成区域,在该区域内获取传感器位置和热力信息,并将这些信息存储在 templateList 数组中。2.将数组传入 Thermodynamic3d() 方法中并配置渐变颜色、透明度等相关信息生成热力图渲染数据。3.使用 createThermodynamicNode() 方法按照热力图渲染数据创建热力图。4.将热力图添加到数据容器中。

视频监控

我们通过 addInteractorListener 交互监听器为场景中摄像头模型绑定点击事件,每个摄像头都对应一个监控视频画面,通过点击弹出或关闭,并对窗口中显示的监控画面数量进行了限制,不得超过 4 个否则将不会继续弹出监控画面,避免显示多个画面造成场景遮挡,关键代码如下:

  1. videoVisible(videoName) {
  2. let g2d = this.g2d,
  3. dm2d = g2d.dm();
  4. // 当前选中监控画面
  5. const video = dm2d.getDataByTag(videoName);
  6. if (video) {
  7. const videoList = video.getParent();
  8. const videoRect = video.getRect();
  9. const visible = g2d.isVisible(video);
  10. if (visible) {
  11. // 隐藏选中监控画面,并重新排列监控画面
  12. this.hideVideo(videoList, video, videoRect);
  13. } else {
  14. // 显示选中监控画面,并重新排列监控画面
  15.  
  16. let showVideos = [];
  17. videoList.eachChild(child => {
  18. g2d.isVisible(child) && child !== video && showVideos.push(child)
  19. })
  20. if (showVideos.length < 5) {
  21. video.s('2d.visible', true);
  22. video.setY(util.getVideoListRect().y + (videoRect.height + 5) * showVideos.length);
  23. }
  24. }
  25. }
  26. }
  27.  
  28. hideVideo(parent, video, videoRect) {
  29. parent.eachChild(node => {
  30. const nodeRect = node.getRect();
  31. if (nodeRect.y > videoRect.y) {
  32. node.setY(nodeRect.y - nodeRect.height)
  33. }
  34. })
  35. video.s('2d.visible', false)
  36. }

总结

现如今,伴随国民经济的持续高速增长,旅游行业迎来了健康发展的阶段,各大景区每年接待的游客人数都在不断增长,如果不对人流量进行控制的话将会出现许多隐患。本次示例效果均采用 HT 提供的 api 进行代码开发,旨在定制一套以人流量监测为中心的集监控与防患应急于一体的景点 3D 实时监控系统,也欢迎对 HT 感兴趣的伙伴给我留言,或者直接访问 官网 查询相关的资料。

基于 HTML5 WebGL 的故宫人流量动态监控系统的更多相关文章

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

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

  2. 基于 HTML5 WebGL 的 3D 棉花加工监控系统

    前言 现在的棉花加工行业还停留在传统的反应式维护模式当中,当棉花加下厂的设备突然出现故障时,控制程序需要更换.这种情况下,首先需要客户向设备生产厂家请求派出技术人员进行维护,然后生产厂家才能根据情况再 ...

  3. 基于 HTML5 WebGL 的民航客机飞行监控系统

    前言 前些日子出差,在飞机上看到头顶的监控面板,除了播放电视剧和广告之外,还会时不时的切换到一个飞机航行的监控系统,不过整个监控系统让人感到有一点点的简陋,所以我就突发奇想制作了一个采用 HT for ...

  4. 基于 HTML5 WebGL 的加油站 3D 可视化监控

    前言 随着数字化,工业互联网,物联网的发展,我国加油站正向有人值守,无人操作,远程控制的方向发展,传统的人工巡查方式逐渐转变为以自动化控制为主的在线监控方式,即采用数据采集与监控系统 SCADA.SC ...

  5. 基于 HTML5 WebGL 的 3D 工控裙房系统

    前言 工业物联网在中国的发展如火如荼,网络基础设施建设,以及工业升级的迫切需要都为工业物联网发展提供了很大的机遇.中国工业物联网企业目前呈现两种发展形式并存状况:一方面是大型通讯.IT企业的布局:一方 ...

  6. 基于 HTML5 WebGL 的 3D 仪表数据监控

    工控仪表重点发展基于现场总线技术的主控系统装置及智能化仪表.特种和专用自动化仪表:全面扩大服务领域,推进仪器仪表系统的数字化.智能化.网络化,完成 自动化仪表从模拟技术向数字技术的转变:推进具有自主版 ...

  7. 基于 HTML5 WebGL 的污水处理厂泵站自控系统

    前言 一道残阳铺水中,半江瑟瑟半江红.随着城市建设的迅速发展,每年都有大量新建管网水管通水运行.城市中有大量的排水设备,形成相应的城市排水系统,排水系统由检查井.排水泵站.污水处理厂.雨水口.排放口等 ...

  8. 基于 HTML5 + WebGL 的宇宙(太阳系) 3D 可视化系统

    前言 近年来随着引力波的发现.黑洞照片的拍摄.火星上存在水的证据发现等科学上的突破,以及文学影视作品中诸如<三体>.<流浪地球>.<星际穿越>等的传播普及,宇宙空间 ...

  9. HTML5+WebGL 的加油站 3D 可视化监控

    前言 随着数字化,工业互联网,物联网的发展,我国加油站正向有人值守,无人操作,远程控制的方向发展,传统的人工巡查方式逐渐转变为以自动化控制为主的在线监控方式,即采用数据采集与监控系统 SCADA.SC ...

随机推荐

  1. 等效燃油消耗ECMS与庞德里亚金最小值原理PMP中协同状态的关系

    今儿阅读了一篇文献——<车联网环境下并联混合动力客车控制策略优化研究>,是北理的博士所写的,内容比较翔实.主要是里面的关于ECMS和庞德里亚金最小值原理(PMP)的关系推导很让人印象深刻, ...

  2. Intellij IDEA 干货分享

    更多视频详情:https://www.bilibili.com/video/av89385013/ Intellij IDEA 真是越用越强大 它总是在我们写代码的时候 不时给我们来个小惊喜 出于对 ...

  3. Nginx之反向代理配置(一)

    前文我们聊了下Nginx作为web服务器配置https.日志模块的常用配置.rewrite模块重写用户请求的url,回顾请参考https://www.cnblogs.com/qiuhom-1874/p ...

  4. 沃土前端系列 - HTML常用标签

    html是什么 HTML是Hyper Text Markup Language的缩写,中文的意思是"超文本标记语言",它是制作网页的标准语言.由于网页中不仅包含普通文本,还包含超文 ...

  5. 适配iphoneX

    tips iphone6设备宽高为375×667,屏幕分辨率为750×1334,故其设备像素比(dpr)为2.iphoneX的设备宽高375*812,屏幕分辨率为1125x2436,故dpr=3 适配 ...

  6. jdk1.5之后,classpath不需要再配置

    1.5之后更智能了. 设置了classpath之后,当前目录失效(在classpath中没找到就报错,不会再在当前目录继续找),所以要把当前目录添加到classpath,永久环境变量添加 .,临时环境 ...

  7. Object-Oriented Programming Summary Ⅱ

    电梯作业总结博客 17373492 电梯,多线程学习中的 "HelloWorld",早在大一就有所耳闻,以至于在坐电梯的时候就思考过:电梯需要怎么写呢? 0. 前言: 偶然的机会, ...

  8. 汇编语言-[bx]和loop指令和多个段

    5.1 [BX]和内存单元的描述 要完成描述一个内存单元,需要两种信息: 内存单元的地址: 可以用 [0] 表示一个内存单元, 0 表示单元的偏移地址,段地址默认在 ds 中: 同样也可以用 [bx] ...

  9. 2019年高校微信小程序开发大赛学习笔记

    学做小程序(学堂在线笔记)一.传统布局 text-align:center //水平居中 margin-bottom: 60px //设置间距 二.弹性盒子布局 display:flex; flex- ...

  10. web安全测试(上)

    前情提要: 公司的安全测试一直是安全部经理全权负责,测试部只做功能和自动化. 但是2019是公司业绩腾飞的一年,业务量越来越大了,安全部经理实在做不过来. 于是他给整个测试部培训<安全测试> ...