相信从事过数据可视化开发的你对大屏并不陌生,那么开发一个酷炫的大屏一定是很多数据可视化开发者想要做的事情。

我们使用three.js,大约一周的时间开发出了一个酷炫的数据可视化大屏:


1. 前言

由于篇幅问题,整篇会分为两个部分,围绕以下几个核心分享:

  • 【一】

    • 地球的实现

    • 地球可点击的交互逻辑

    • 飞线的实现

  • 【二】

    • 平面地图的实现

    • 柱体的实现

    • 性能优化

    • 地图相关问题

涉及到的知识点:

  • GLSL:着色器在各3D对象中的应用

  • THREE.ShaderMaterial:three.js与着色器的复合应用

  • THREE.Texture:贴图与着色器的复合应用

  • THREE.CubicBezierCurve3:三次三维空间贝塞尔曲线

  • THREE.CylinderGeometry:如何基于数据为圆柱几何体上色

使用的技术栈:

  • vue

  • webpack

  • three.js

  • antv

  • d3.js

2. 酷炫的地球

在我们的大屏中,酷炫的地球作为颜值担当,有效的撑起了场面。

2.1 地球

地球使用THREE.ShaderMaterial实现,它由多张贴图材质构成,而非使用多面模型。

他承载了球体本身点击交互

地球由五张贴图组成:

贴图1 : mapIndex

这张索引贴图为每个国家分配 1 - 255 之间不同的索引颜色。部分国家颜色只是看似相近,实际数值不同。

非陆地部分的颜色为 0

他用于我们在做点击交互时识别点击位置的国家GLSL为选择的国家上色

在使用时需要注意:贴图不能出现模糊、羽化等现象,使用photoshop编辑时要使用铅笔笔触。否则会影响到片元着色器的计算。

贴图2 : lookup

他是一张 1 x 256 大小的索引贴图。初始状态下第1个色值是 #000000 ,剩下2 - 256#FFFFFF的。

他需要随着交互动态变化,所有由canvas生成。

const lookupCanvas = document.createElement('canvas');
lookupCanvas.width = 256;
lookupCanvas.height = 1;const lookupTexture = new THREE.Texture(lookupCanvas);
lookupTexture.magFilter = THREE.NearestFilter;
lookupTexture.minFilter = THREE.NearestFilter;
lookupTexture.needsUpdate = true;

下标为 0 的像素对应mapIndex大海的颜色 #000000

下标在 1 - 255 之间的像素与mapIndex不同国家的索引颜色对应。

在触发点击交互获取到对应国家所代表的颜色时,改变其在lookup贴图对应下标位置的颜色,这里我们定义为#CCCCCC对应float 0.8

这样在片元着色器运行时我们可以区分国家海洋被选中的国家来进行不同的渲染计算。

uniform sampler2D mapIndex;
uniform sampler2D lookup;
varying vec2 vUv;void main() {vec4 earthColor = vec4(0.0);vec4 mapColor = texture2D(mapIndex, vUv);float indexedColor = mapColor.x;vec4 lookupColor = texture2D(lookup, vec2(indexedColor, 0.0)); // 使用mapIndex与lookup对应if (lookupColor.x == 1.0) {        // 国家 #FFFFFF} else if (lookupColor.x == 0.0) { // 海洋 #000000} else if (lookupColor.x == 0.8) { // 被选中的国家 #CCCCCC}gl_FragColor = earthColor;
}

贴图3 : outline

这张贴图勾勒出了国家边界。

这张索引贴图不同于mapIndex,他可以进行模糊处理,并且要尽量保证#FFFFFF颜色的线条不超过 1 像素。

我们可以在片元着色器计算时通过数值判断来控制边界粗细。

但颜色如果是#FFFFFF,我们将只能控制这部分边界是显示还是不显示。

uniform sampler2D outline;
varying vec2 vUv;void main() {float outlineColor = texture2D(outline, vUv).xif (lookupColor.x == 1.0) { // 国家 #FFFFFFif (outlineColor > 0.3) { // 此处过滤数值越大 国界越细} else { // 国家颜色}} else if (lookupColor.x == 0.0) { // 海洋 #000000} else if (lookupColor.x == 0.8) { // 被选中的国家 #CCCCCCif (outlineColor > 0.0) { // 0.0 代表显示当前国家区域内所有的边界} else {}}gl_FragColor = earthColor;
}

贴图4 : textTexture

这张贴图仅仅是写了几个国家的名字。

使用时文字贴图会优先所有判断,从而显示在球体上。

需要注意的是:球体会按极坐标使用贴图,所以写在离南北极较近地方的文字要随着纬度拉的胖一些。

uniform sampler2D textTexture;
varying vec2 vUv;
void main() {vec4 text = texture2D(textTexture, vUv);if (lookupColor.x == 1.0) {} else if (lookupColor.x == 0.0) {} else if (lookupColor.x == 0.8) {}if (text.w > 0.3) { // 此处过滤数值越大 文字越细earthColor = vec4(0.7, 0.7, 0.7, 1); // 文字颜色;覆盖前面的颜色计算}
}

贴图5 : depthTexture

这张贴图描绘了海洋的深度。

在片元着色器计算时判断为海洋的位置将会使用海洋的贴图。

uniform sampler2D depthTexture;
varying vec2 vUv;void main() {vec4 depth = texture2D(depthTexture, vUv);if (lookupColor.x == 0.0) { // 海洋 #000000earthColor = vec4(mix(vec3(1.0), depth.xyz, 0.86), 1.0); // mix混合白色让海洋亮一些}gl_FragColor = earthColor;
}

其他uniform

除了贴图外,我们还要定义 5 个颜色与 1 个布尔状态。他们分别是:

  • surfaceColor: 正常陆地颜色

  • selectedColor: 选中国家后的陆地颜色

  • lineColor: 正常国界颜色

  • lineSelectedColor: 选中国家的国界颜色

  • u_lightColor: 常驻渐变色

  • flag: 正在进行点击交互的标记

完整的着色器代码

片元着色器
uniform sampler2D mapIndex;
uniform sampler2D lookup;
uniform sampler2D outline;
uniform sampler2D textTexture;
uniform sampler2D depthTexture;
uniform float outlineLevel;uniform vec3 surfaceColor;
uniform vec3 lineColor;
uniform vec3 lineSelectedColor;
uniform vec3 selectedColor;
uniform vec3 u_lightColor;
uniform float flag;vec3 u_lightDirection = vec3(0.0, 1.0, 0.0); //光的入射方向
varying vec3 vNormal;
varying vec2 vUv;void main() {vec4 mapColor = texture2D(mapIndex, vUv);vec4 text = texture2D(textTexture, vUv);vec4 depth = texture2D(depthTexture, vUv);float indexedColor = mapColor.x;vec4 lookupColor = texture2D(lookup, vec2(indexedColor, 0.0));float outlineColor = texture2D(outline, vUv).x;float diffuse = lookupColor.x + indexedColor + outlineColor;vec4 earthColor = vec4(0.0);if (flag == 1.0) {if (lookupColor.x == 1.0) { // 国家 #FFFFFFif (outlineColor > 0.3) { // 此处过滤数值越大 国界越细earthColor = vec4(lineColor, 0.8); // 国界的颜色} else {earthColor = vec4(mix(surfaceColor, vec3(indexedColor), 0.0), 1.0); // 国家的颜色}} else if (lookupColor.x == 0.0) { // 海洋 #000000earthColor = vec4(mix(vec3(1.0), depth.xyz, 0.86), 1.0); // mix混合白色让海洋亮一些vec3 faceNormal = normalize(vNormal); // 表面的法向量float nDotL = max(dot(u_lightDirection, faceNormal), 0.0); // 获取入射光线与法向量的夹角vec4 AmbientColor = vec4(u_lightColor, 1.0); // 环境光vec4 diffuseColor = vec4(u_lightColor, 1.0) * nDotL; // 漫反射光的颜色earthColor = earthColor * (AmbientColor + diffuseColor);} else if (lookupColor.x == 0.8) { // 点击后选中的背景色if (outlineColor > 0.0) {earthColor = vec4(lineSelectedColor, 1); // 选中国家的国界颜色} else {earthColor = vec4(selectedColor, 1); // 选中国家后的陆地颜色}}if (text.w > 0.3) { // 此处过滤数值越大 文字越细earthColor = vec4(0.7, 0.7, 0.7, 1);}} else { // flag == 0.0 表示正在进行点击计算earthColor = vec4(vec3(diffuse), 1.0);}gl_FragColor = earthColor;
}
顶点着色器
varying vec2 vUv;
varying vec3 vNormal;
void main() {vUv = uv;vNormal = normal;gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

2.2 点击交互

可视化不仅仅是静态的图形数据,还需要与人交互。

所以这个酷炫的地球就需要支持选中国家并且获取到国家名称。

交互逻辑

地球的交互逻辑如下:

  1. 监听到鼠标点击

  • 填充整个画布为黑色

  • 设置uniforms.flag.value = 0;

  • 手动进行一次render(需要注意在这之前要隐藏其他所有3D对象)

  • 使用gl.readPixels带入点击信息来获取鼠标落点的颜色

  • 通过映射表获取到对应的国家ID(可以准备更多的映射信息)

  • 根据1-255的落点色值为lookupCanvas对应位置的像素填充#CCCCCC

  • 恢复正常渲染

映射表

{1:'PE',2:'BF',3:'FR',4:'LY',5:'BY',6:'PK',7:'ID',8:'YE',9:'MG',10:'BO',11:'CI',12:'DZ',13:'CH',14:'CM',15:'MK',16:'BW',17:'UA',18:'KE',19:'TW',20:'JO',21:'MX',22:'AE',23:'BZ',24:'BR',25:'SL',26:'ML',27:'CD',28:'IT',29:'SO',30:'AF',31:'BD',32:'DO',33:'GW',34:'GH',35:'AT',36:'SE',37:'TR',38:'UG',39:'MZ',40:'JP',41:'NZ',42:'CU',43:'VE',44:'PT',45:'CO',46:'MR',47:'AO',48:'DE',49:'SD',50:'TH',51:'AU',52:'PG',53:'IQ',54:'HR',55:'GL',56:'NE',57:'DK',58:'LV',59:'RO',60:'ZM',61:'IR',62:'MM',63:'ET',64:'GT',65:'SR',66:'EH',67:'CZ',68:'TD',69:'AL',70:'FI',71:'SY',72:'KG',73:'SB',74:'OM',75:'PA',76:'AR',77:'GB',78:'CR',79:'PY',80:'GN',81:'IE',82:'NG',83:'TN',84:'PL',85:'NA',86:'ZA',87:'EG',88:'TZ',89:'GE',90:'SA',91:'VN',92:'RU',93:'HT',94:'BA',95:'IN',96:'CN',97:'CA',98:'SV',99:'GY',100:'BE',101:'GQ',102:'LS',103:'BG',104:'BI',105:'DJ',106:'AZ',107:'MY',108:'PH',109:'UY',110:'CG',111:'RS',112:'ME',113:'EE',114:'RW',115:'AM',116:'SN',117:'TG',118:'ES',119:'GA',120:'HU',121:'MW',122:'TJ',123:'KH',124:'KR',125:'HN',126:'IS',127:'NI',128:'CL',129:'MA',130:'LR',131:'NL',132:'CF',133:'SK',134:'LT',135:'ZW',136:'LK',137:'IL',138:'LA',139:'KP',140:'GR',141:'TM',142:'EC',143:'BJ',144:'SI',145:'NO',146:'MD',147:'LB',148:'NP',149:'ER',150:'US',151:'KZ',152:'AQ',153:'SZ',154:'UZ',155:'MN',156:'BT',157:'NC',158:'FJ',159:'KW',160:'TL',161:'BS',162:'VU',163:'FK',164:'GM',165:'QA',166:'JM',167:'CY',168:'PR',169:'PS',170:'BN',171:'TT',172:'CV',173:'PF',174:'WS',175:'LU',176:'KM',177:'MU',178:'FO',179:'ST',181:'DM',182:'TO',183:'KI',184:'FM',185:'BH',186:'AD',187:'MP',188:'PW',189:'SC',190:'AG',191:'BB',192:'TC',193:'VC',194:'LC',195:'YT',196:'VI',197:'GD',198:'MT',199:'MV',200:'KY',201:'KN',202:'MS',203:'BL',204:'NU',205:'PM',206:'CK',207:'WF',208:'AS',209:'MH',210:'AW',211:'LI',212:'VG',213:'SH',214:'JE',215:'AI',217:'GG',218:'SM',219:'BM',220:'TV',221:'NR',222:'GI',223:'PN',224:'MC',225:'VA',226:'IM',227:'GU',228:'SG',}

弊端

这一逻辑依赖于第四步骤的手动render。

而如果快速的点击来触发多次render将会打破正常的动画帧率产生卡顿。

而如果在渲染性能差帧率低的机器上触发一次也有可能会导致轻危的卡顿。

并且你无法通过监听mousemove中来真正的响应鼠标滑动事件,因为mousemove一秒钟内触发的次数甚至会超过动画帧率。造成一秒渲染120+帧的明显卡顿。

2.3 飞线

飞线是用来表达具有目的性的数据。

使用THREE.ShaderMaterial 配合 THREE.CubicBezierCurve3 实现。

实现原理是在一条由许很多很多的点组成的贝塞尔曲线路径上不断的改变顶点的透明度与大小,达到线在飞的效果。

顶点着色器是飞线的重头戏。

路径计算

在进行贝塞尔曲线之前,我们需要对位置数据进行一次处理。

因为飞线要映射在球体上,而后台数据是不可能直接返回Vector3(x, y, z)的数据供你使用的。

所以我们要进行一次转换,我们使用最简单的三角函数来进行转换:

/**
* 将平面经纬度转换为实际 x, y, z 坐标
* @param {Number} lng 经度
* @param {Number} lat 纬度
* @param {Number} radius 球体半经
*/
function getSpherePosition(lng = 0, lat = 0, radius = 100) {if (lng < 0) {lng += 360;}if (lat > 0) {lat += 2;}const y = radius * Math.sin((lat * Math.PI) / 180);const zx = radius * Math.cos((lat * Math.PI) / 180);const x = zx * Math.sin((lng * Math.PI) / 180);const z = zx * Math.cos((lng * Math.PI) / 180);return new THREE.Vector3(x, y, z);
}
getSpherePosition(116.3, 39.9, 100) // => {x: 66.72652011739466, y: 66.78325554710466, z: -32.97830031328238}

贝塞尔曲线

// 三维三次贝塞尔曲线(v0起点,v1第一个控制点,v2第二个控制点,v3终点)
let v0, v1, v2, v3;
// 地球的半经是 100
v0 = getSpherePosition(start_lng, start_lat, 100);
v3 = getSpherePosition(end_lng, end_lat, 100);const angle = v0.angleTo(v3);
let vtop = v0.clone().add(v3);
vtop = vtop.normalize().multiplyScalar(100);
let n;if (angle <= 1) {n = (params.globeRadius / 5) * angle;
} else if (angle > 1 && angle < 2) {n = (params.globeRadius / 5) * Math.pow(angle, 2);
} else {n = (params.globeRadius / 5) * Math.pow(angle, 1.5);
}v1 = v0.clone().add(vtop).normalize().multiplyScalar(100 + n);
v2 = v3.clone().add(vtop).normalize().multiplyScalar(100 + n);const curve = new THREE.CubicBezierCurve3(v0, v1, v2, v3);
const points = curve.getPoints(500);
const geometry = new THREE.Geometry().setFromPoints(points); // 带入贝塞尔曲线的顶点生成geometry
const { length } = points;
const percents = new Float32Array(length);
for (let i = 0; i < length; i += 1) {percents[i] = i / length;
}
geometry.addAttribute('percent', new THREE.BufferAttribute(percents, 1)); // 传入500个从0到1的percent供顶点着色器使用

顶点着色器

顶点着色器是实现飞线的核心。

attribute float percent; // 针对着色器`逐顶点运行`特性的辅助参数uniform float time;      // 飞线当前的进度。这个参数将会随着动画从0到1不断增加
uniform float number;    // 飞线路径长度
uniform float speed;     // 飞线运行速度
uniform float length;    // 飞线拖尾长度
uniform float size;      // 飞线粗细varying float opacity;void main() {float l = clamp(1.0 - length, 0.0, 1.0);// 计算公式gl_PointSize = clamp(fract(percent * number + l - time * number * speed) - l, 0.0, 1.0) * size * (1.0 / length);opacity = gl_PointSize / size; // 供片元着色器使用,可自行修改以控制飞线拖尾的形状gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

片元着色器

varying float opacity;
uniform vec3 color;    // 飞线颜色void main() {if (opacity <= 0.2) {discard;}gl_FragColor = vec4(color, 1.0);
}

弊端

通过顶点实现的飞线,在顶点密度不足的情况下会出现异常。

这是因为着色器会按照屏幕像素来渲染大小,而不会因为相机的远近变化来放大缩小。

解决的办法有两种:

  • 增加顶点的密度

  • 更换飞线实现方式(使用官方开发的meshline或自行开发)

小结

本章主要讲述了textureuniformattribute三者与GLSL配合使用的场景,并延伸出索引贴图的解决方案。

下一章将会讲述传统3d平面地图的绘制方法和我们在实现地图相关产品时的其他注意事项。

最后

  • 欢迎加我微信(winty230),拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

点个在看支持我吧

如何1人5天开发完3D数据可视化大屏 【一】相关推荐

  1. 如何保证两个不同宽高的canvas用同一组坐标正常显示_如何1人5天开发完3D数据可视化大屏 【一】...

    相信从事过数据可视化开发的你对大屏并不陌生,那么开发一个酷炫的大屏一定是很多数据可视化开发者想要做的事情. 我们使用three.js,大约一周的时间开发出了一个酷炫的数据可视化大屏: 1. 前言 由于 ...

  2. Django开发数据可视化大屏-JS绘制大屏动态背景-(视图模板制作)

    查看本文前请先查看 Django开发数据可视化大屏-项目启动配置 通过前面的文章,我们已经创建了一个Django简单项目,并且做了相关的配置,今天我们来制作视图模板,通过JS绘制3D动态背景效果. 我 ...

  3. 数据可视化大屏电商数据展示平台开发实录(Echarts柱图曲线图、mysql筛选统计语句、时间计算、大数据量统计)

    数据可视化大屏电商数据展示平台 一.前言 二.项目介绍 三.项目展示 四.项目经验分享 4.1 翻牌器 4.1.1 翻牌器-今日实时交易 4.1.2.翻牌器后端统计SUM函数的使用 4.2 不同时间指 ...

  4. 基于WebGL的三维数据可视化大屏开发流 ThingJS

    三维数据大屏可视化系统包含多源数据连接.生成二维/三维视图.构建可视化大屏.大屏功能应用等一体化服务,基于多年可视化项目经验,ThingJS平台得出从数据源上传到可视化大屏应用的完整流程,供参考. 多 ...

  5. 不懂开发,也可以开发酷炫的可视化大屏

    在软件开发.项目交付的过程中,很多时候需要开发酷炫的大屏驾驶舱. 别人开发的可视化大屏驾驶舱,是这样的: 可是当自己上手开发的时候,可能是这样的: 如何也能快速开发出酷炫的大屏呢? 可通过一站式可视化 ...

  6. 看完上汽制动的数字化,才发现以前的数据可视化大屏都白做了

    2019年,汽车行业正在发生巨大的变化. 市场竞争也变得更为激烈.去年以来一批重大的国家政策和汽车行业政策出台,对汽车市场产生了重大影响.如何应对如此复杂的局面,各车企及相关产业链都在寻求转型.升级, ...

  7. 润乾报表列太多导致渲染速度慢_报表自动化: 如何选择可视化大屏开发利器

    报表自动化: 如何选择可视化大屏开发利器​www.coologic.cn 我们在前文完成了各种数据的准备:原始数据.指标数据.报表表格等等,但仍然无法解决"阅者"难以理解庞大数据. ...

  8. 南通数字孪生智能工厂,数字工厂智能车间建设,3d可视化工业建模,三维数据交互大屏系统开发

    南通数字孪生智能工厂,数字工厂智能车间建设,3d可视化工业建模,三维数据交互大屏系统开发."中国制造2025"与"互联网+"等国家级行动纲领的颁发,给制造业带来 ...

  9. 炫酷大屏demo_那些炫酷高端的可视化大屏,是如何开发出来的?

    最近,我不止一次地听到来自粉丝的需求--可视化大屏怎么做? 难道是大家年底都在冲业绩? 既然说到可视化驾驶舱大屏,必然是我很感兴趣的了,下面是自己多年的教程和心得分享给大家. 工具选择 Excel.p ...

最新文章

  1. android sqlite 怎么写入存储时间
  2. sublime运行python代码python没显示_解决windows下Sublime Text 2 运行 PyQt 不显示的方法分享...
  3. Live Maps中国更新-为北京增加实时交通信息
  4. VB 文件编码互换模块(支持 Ansi,UTF-8,Unicode(little endian),Unicode big endian)
  5. There is insufficient memory for the Java Runtime Environment to continue.
  6. 信号灯绿波服务器,主干道绿波与红波控制策略
  7. 【毕业季】一个普通大二学生的迷茫与展望
  8. Docker常用命令(基础)
  9. 第三十四章 苏西受伤
  10. less面试_想获得理想工作?面试时千万不要说这七句话
  11. SuperMap Hi-Fi 3D SDK 11i(2022) for Unity插件开发——选中对象隐藏
  12. 微型计算机ccc认证依据,3C检测规定
  13. 疫情汹涌,餐饮行业如何修炼内功科学选址?——市场趋势及数据洞察篇
  14. 国内IT公司病种,需要合理协调,共同进步,才能不被嘈乱的世道所唾弃
  15. 学习笔记:全局变量定义“无须”初始化,局部变量必须初始化
  16. 【收藏版】Linux常用命令大全
  17. 铁路工程物资管理软件系统
  18. primary key 主键
  19. 17个非常炫酷的后台管理系统模板
  20. jre7换jre8问题

热门文章

  1. 线上配镜新方式:眼镜直通车竞品分析报告
  2. 2017 Multi-University Training Contest - Team 8:Fleet of the Eternal Throne(AC自动机)
  3. 转换金额为大写人民币-Java
  4. 用了TCP协议,就一定不会丢包吗?
  5. Python 组织机构代码证校验
  6. Apache Kylin(一)
  7. 通过ip无法获得计算机名称,电脑获取不到IP地址如何解决
  8. Andriod 虚拟机
  9. java long型数据做余数运算_Java数据类型与运算符
  10. Android之讯飞语音-文字转语音(不用另外安装语音合成包apk)遇到的问题