这是最近公司的一个项目。客户的需求是基于总公司和子公司的数据,开发一个数据展示大屏。 大屏两边都是一些图表展示数据,中间部分是一个三维中国地图,点击中国地图的某个省份,可以下钻到省份地图的展示。 地图上,会做一些数据的标注,信息标牌。 如下图所示:

本文将对一些技术原理进行分享。

2d图表

2d图表部分,主要通过echart图表进行开发,另外还会涉及到一些icon 文字的展示。 这个部分相信大部分前端人员都知道如何进行开发,可能需要的就是开发人员对于颜色,字体等有较好的敏感性,可以最大程度还原设计搞。

鉴于大家都比较熟知,不再详细说明。

三维地图的展示

对于中间的三维地图部分。 我们一般有几种方式来实现。

  1. 建模人员对地图部分进行建模
  2. 通过json数据生成三维模型
  3. 通过svg图片生产三维模型。

其中方式1能达到最好的效果,毕竟手动建模了,需要的效果都可以通过建模师智慧的双手进行调整。但是工作量相对来说较大,需要建立中国地图和各个省份的地图。 所以我们最终放弃了建模的这种思路。

通过json数据生成三维地图

首先要获取json数据。

通过datav可以获取中国地图的json数据,参考如下连接

http://datav.aliyun.com/portal/school/atlas/area_selector

获取数据之后,通过解析json数据,然后通过threejs的ExtrudeGeometry生成地图模型。代码如下所示:

 let jsonData = await (await fetch(jsonUrl)).json();
// console.log(jsonData);
let map = new dt.Group();
if (type && type === "world") {
jsonData.features = jsonData.features.filter(
(ele) => ele.properties.name === "China"
);
}
jsonData.features.forEach((elem, index) => {
if (filter && filter(elem) == false) {
return;
}
if (!elem.properties.name) {
return;
}
// 定一个省份3D对象
const province = new dt.Group();
// 每个的 坐标 数组
const coordinates = elem.geometry.coordinates;
const color = COLOR_ARR[index % COLOR_ARR.length];
// 循环坐标数组
coordinates.forEach((multiPolygon, index) => {
if (elem.properties.name == "海南省" && index > 0) {
return;
}
if (elem.properties.name == "台湾省" && index > 0) {
return;
}
if (elem.properties.name == "广东省" && index > 0) {
return;
}
multiPolygon.forEach((polygon) => {
const shape = new dt.Shape(); let positions = [];
for (let i = 0; i < polygon.length; i++) {
let [x, y] = projection(polygon[i]); if (i === 0) {
shape.moveTo(x, -y);
}
shape.lineTo(x, -y); positions.push(x, -y, 4);
} const lineMaterial = new dt.LineBasicMaterial({
color: "white",
});
const lineGeometry = new dt.LineXGeometry();
// let attribute = new dt.BufferAttribute(new Float32Array(positions), 3);
// lineGeometry.setAttribute("position", attribute);
lineGeometry.setPositions(positions); const extrudeSettings = {
depth: 4,
bevelEnabled: false,
bevelSegments: 5,
bevelThickness: 0.1,
}; const geometry = new dt.ExtrudeGeometry(shape, extrudeSettings);
// console.log("geometyr", geometry);
const material = new dt.StandardMaterial({
metalness: 1,
// color: color,
map: texture,
transparent: true,
}); let material1 = new dt.StandardMaterial({
// polygonOffset: true,
// polygonOffsetFactor: 1,
// polygonOffsetUnits: 1,
metalness: 1,
roughness: 1,
color: color, //"#3abcbd",
}); material1 = createSideShaderMaterial(material1); const mesh = new dt.Mesh(geometry, [material, material1]);
if (index % 2 === 0) {
// mesh.scale.set(1, 1, 1.2);
} mesh.castShadow = true;
mesh.receiveShadow = true;
mesh._color = color;
mesh.properties = elem.properties;
if (!type) {
province.add(mesh);
} const matLine = new dt.LineXMaterial({
polygonOffset: true,
polygonOffsetFactor: -1,
polygonOffsetUnits: -1,
color: type === "world" ? "#00BBF4" : 0xffffff,
linewidth: type === "world" ? 3.0 : 0.25, // in pixels
vertexColors: false,
dashed: false,
});
matLine.resolution.set(graph.width, graph.height);
line = new dt.LineX(lineGeometry, matLine);
line.computeLineDistances();
province.add(line);
});
}); // 将geo的属性放到省份模型中
province.properties = elem.properties;
if (elem.properties.centorid) {
const [x, y] = projection(elem.properties.centorid);
province.properties._centroid = [x, y];
} map.add(province);

中国地图的json数据,实际包括的是每个省份的数据。

上述代码生成中国地图以及省之间的轮廓线。

其中projection 是投影函数,转换经纬度坐标未平面坐标,用的是d3这个库:

const projection = d3
.geoMercator()
.center([104.0, 37.5])
.scale(80)
.translate([0, 0]);

按照设计稿,还需生成整个中国地图的外轮廓。这种情况下,我们先获取world.json,然后只获取中国的部分,通过这个部分来生成轮廓线。

最终效果如下:

可以看出,通过json的方式生产地图,世界地图的json数据和中国地图的json数据,边缘的贴合度并不高,因此外边缘轮廓和地图块不能很好的融合在一块。

基于此,需要找新的方案。

通过svg数据生成三维地图

由于有设计师提供设计稿,所以设计师肯定可以提供中国地图的轮廓数据,以及内部的每个省份的轮廓数据。拿到设计的svg后,对svg路径进行解析,然后通过ExtrudeGeometry生成地图块对下,通过line生成轮廓线。

 let childNodes = svg.childNodes;
childNodes.forEach((child) => {
readSVGPath(child, graph, group);
});
if (svg.tagName == "path") {
const shape = getShapeBySvg(svg);
// let shape = $d3g.transformSVGPath(pathStr);
const extrudeSettings = {
depth: 15,
bevelEnabled: false,
bevelSegments: 5,
bevelThickness: 0.1,
}; const color = COLOR_ARR[parseInt(Math.random() * 3) % COLOR_ARR.length];
const geometry = new dt.ExtrudeGeometry(shape, extrudeSettings);
let center = new dt.Vec3();
// console.log(geometry.getBoundingBox().getCenter(center));
// geometry.translate(-center.x, -center.y, -center.z);
geometry.scale(1, -1, -1);
geometry.computeVertexNormals();
// console.log("geometry", geometry);
const material = new dt.StandardMaterial({
metalness: 1,
// color: color,
// visible: false,
map: window.texture,
}); let material1 = new dt.StandardMaterial({
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1,
metalness: 1,
roughness: 1,
color: color, //"#3abcbd",
}); material1 = createSideShaderMaterial(material1); const mesh = new dt.Mesh(geometry, [material, material1]);
group.add(mesh);

其中解析svg路径的代码如下:

function getShapeBySvg(svg) {
let pathStr = svg.getAttribute("d");
let province = svg.getAttribute("province");
let commonds = new svgpathdata.SVGPathData(pathStr).commands; const shape = new dt.Shape();
let lastC, cmd, c;
for (let i = 0; i < commonds.length; i++) {
cmd = commonds[i];
let relative = cmd.relative; if (relative) {
c = copy(cmd);
let x = cmd.x || 0;
let y = cmd.y || 0;
let lx = lastC.x || 0;
let ly = lastC.y || 0;
c.x = x + lx;
c.y = y + ly;
c.x1 = c.x1 + lx;
c.x2 = c.x2 + lx;
c.y1 = c.y1 + ly;
c.y2 = c.y2 + ly;
} else {
c = cmd;
}
if (lastC) {
let lx = lastC.x,
ly = lastC.y;
if (
Math.hypot(lx - c.x, ly - c.y) < 0.2 &&
province == "内蒙" &&
[16, 32, 128, 64, 512, 4, 8].includes(c.type)
) {
console.log(c.type);
continue;
}
}
if (c.type == 2) {
shape.moveTo(c.x, c.y);
} else if (c.type == 16) {
shape.lineTo(c.x, c.y);
} else if (c.type == 32) {
shape.bezierCurveTo(c.x1, c.y1, c.x2, c.y2, c.x, c.y);
// shape.lineTo(c.x, c.y);
} else if (c.type == 128 || c.type == 64) {
shape.quadraticCurveTo(c.x1 || c.x2, c.y1 || c.y2, c.x, c.y);
// shape.lineTo(c.x, c.y);
} else if (c.type == 512) {
// shape.absellipse(c.x, c.y, c.rX, c.rY, 0, Math.PI * 2, true);
shape.lineTo(c.x, c.y);
} else if (c.type == 4) {
c.y = lastC.y;
shape.lineTo(c.x, lastC.y);
} else if (c.type == 8) {
c.x = lastC.x;
shape.lineTo(lastC.x, c.y);
} else if (c.type == 1) {
// shape.closePath();
} else {
// console.log(c);
}
lastC = c;
}
return shape;
}

其中里面涉及到相对定位的概念,一个cmd的坐标是相对于上一个坐标的,而不是绝对定位。这就需要我们在解析的时候,通过累加的方式获取绝对定位坐标。

另外cmd的type主要包括:

  //   ARC: 512
// CLOSE_PATH: 1
// CURVE_TO: 32
// DRAWING_COMMANDS: 1020
// HORIZ_LINE_TO: 4
// LINE_COMMANDS: 28
// LINE_TO: 16
// MOVE_TO: 2
// QUAD_TO: 128
// SMOOTH_CURVE_TO: 64
// SMOOTH_QUAD_TO: 256
// VERT_LINE_TO: 8

通过Shape的moveTo,lineTo,bezierCurveTo,quadraticCurveTo等等与之对应。

最终效果如下图:



可以看出轮廓线更加圆滑,外轮廓和地图块的贴合度更高。

这是我们项目最终采用的技术方案。

侧边渐变效果

上述两种方案的效果图,可以看出侧边地图的侧面都有渐变效果,这种是通过定制threejs的材质的shader来实现的。大致代码如下:


function createSideShaderMaterial(material) {
material.onBeforeCompile = function (shader, renderer) {
// console.log(shader.fragmentShader);
shader.vertexShader = shader.vertexShader.replace(
"void main() {",
"varying vec4 vPosition;\nvoid main() {"
);
shader.vertexShader = shader.vertexShader.replace(
"#include <fog_vertex>",
"#include <fog_vertex>\nvPosition=modelMatrix * vec4( transformed, 1.0 );"
); shader.fragmentShader = shader.fragmentShader.replace(
"void main() {",
"varying vec4 vPosition;\nvoid main() {"
); shader.fragmentShader = shader.fragmentShader.replace(
"#include <transmissionmap_fragment>",
`
#include <transmissionmap_fragment>
float z = vPosition.z;
float s = step(2.0,z);
vec3 bottomColor = vec3(.0,1.,1.0); diffuseColor.rgb = mix(bottomColor,diffuseColor.rgb,s);
// float r = abs( 1.0 * (1.0 - s) + z * (0.0 - s * 1.0) + s * 4.0) ;
float r = abs(z * (1.0 - s * 2.0) + s * 4.0) ;
diffuseColor.rgb *= pow(r, 0.5 + 2.0 * s); // float c =
`
);
}; return material;
}

通过material.onBeforeCompile方法实现材质的动态更改,然后通过z坐标的高度进行颜色的渐变差值运算。

三维地图的贴图

上面实现的效果,都是简单的颜色。没有贴图效果,而设计师提供的原型是有渐变效果的:

这需要我们的贴图来进行解决。 但是贴图并不简单,涉及到uv的offset和repeat的计算。 通过计算整个中国地图的boundingbox,通过bongdingbox的size 和min 值来设置uv 的offset和repeat,可以很好的对其贴图和模型,如下代码:

 let box = new dt.Box3();
box.setFromObject(map);
et size = new dt.Vec3(),
center = new dt.Vec3();
console.log(box.getSize(size));
console.log(box.getCenter(center));
console.log(box); texture.repeat.set(1 / size.x, 1 / size.y);
texture.offset.set(box.min.x / size.x, box.min.y / size.y);

通过这种方式,贴图可以很好的和模型对齐,最终效果和设计稿差别很小。

三维地图icon标注定位

图片上的图标定位数据是经纬度,所以需要把定位度转换为三维中的坐标。此处使用的是双线性差值。先获取模型左上,右上,左下,右下四个点的经纬度坐标和三维坐标,然后通过双线性差值,结合某个特定点的经纬度值 计算出三维坐标。 这种方式肯定不是最精确的,却是最简单的。如果对于定位的精确性要求不高,可以采用这种方式。

icon动画(APNG)

icon的动画是通过apng的图片实现的。 解析apng的每一帧,然后绘制到canvas上面,作为sprite的贴图,并不断刷新贴图的内容,实现了动效效果。 有关apng的解析,网上有开源的JavaScript的解析包。读者可以自行进行研究,下面是一个参考链接:

https://github.com/movableink/three-gif-loader

其他

其他方面包括

  1. 点击省份下钻 技术实现就是隐藏其他省份模型,显示当前省份模型,并加载当前省份的点位数据。技术思路比较简单。
  2. 鼠标悬浮显示名称等信息 通过div实现信息标签,通过三维坐标转平面坐标的投影算法,计算标签位置,代码如下:
 getViewPosition(vector) {
this.camera.updateMatrixWorld();
var ret = new Vec3();
// ret = this.projector.projectVector(vector, this._camera, ret);
ret = vector.project(this.camera);
ret.x = ret.x / 2 + 0.5;
ret.y = -ret.y / 2 + 0.5;
var point = {
x: (this._canvas.width * ret.x) / this._pixelRatio,
y: (this._canvas.height * ret.y) / this._pixelRatio,
h: this._canvas.height,
};
return point;
}

总结

上面分享的三维地图大屏。涉及到的技术点并不少,包括主要如下技术点:

  • echart使用
  • json解析生成地图projection投影
  • svg 解析生成三维地图模型
  • 动态材质修改
  • 贴图的offset和repeat算法等
  • 经纬度定位,双线性差值
  • 三维的三维坐标转平面坐标的投影算法

最终多个技术的融合,做出了文章开头的效果。

其中比较难的是中间三维地图的生成和效果优化方案,如果有类似需求的读者可以参考。

如果你有好的经验,也欢迎和我交流。关注公号“ITMan彪叔” 可以添加作者微信进行交流,及时收到更多有价值的文章。

threejs三维地图大屏项目分享的更多相关文章

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

    利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果 前言 近日公司接到一个轨道系统的需求,需要将地铁线路及列车实时位置展示在大屏上.既然是大屏项目,那视觉效果当然是第一重点,咱们可以先来看看项 ...

  2. ArcGIS Pro开发Web3D应用(2)——地图分屏对比(多屏对比)思路

    很多应用中都需要用到地图联动.多屏对比.二三维分屏.大屏显示,有图形可视化的地方就有事件响应触发:鼠标按下.移动.鼠标滚轮,由此触发了地图上坐标或范围的变化,将这些变化发送给另一个地图并响应这些变化, ...

  3. echarts解决一些大屏图形配置方案汇总

    本文主要记录使用echarts解决各种大屏图形配置方案. 1.说在前面 去年经常使用echarts解决一些可视化大屏项目,一直想记录下使用经验,便于日后快速实现.正好最近在整理文档,顺道一起记录在博客 ...

  4. 一招教你轻松使用数据可视化BI软件创建旅游消费数据可视化大屏

    灯果数据可视化BI软件是新一代人工智能数据可视化大屏软件,内置丰富的大屏模板,可视化编辑操作,无需任何经验就可以创建属于你自己的大屏.大家可以在他们的官网下载软件.   本文以旅游消费数据可视化大屏为 ...

  5. 不会用数据可视化大屏?一招教你轻松使用数据可视化BI软件创建农业公司运营数据分析大屏

    灯果数据可视化BI软件是新一代人工智能数据可视化大屏软件,内置丰富的大屏模板,可视化编辑操作,无需任何经验就可以创建属于你自己的大屏.大家可以在他们的官网下载软件.   本文以农业公司运营数据分析大屏 ...

  6. 干货!手把手教你使用数据可视化BI软件创建企业变更流程监控大屏

    灯果数据可视化BI软件是新一代人工智能数据可视化大屏软件,内置丰富的大屏模板,可视化编辑操作,无需任何经验就可以创建属于你自己的大屏.大家可以在他们的官网下载软件.   本文以企业变更流程监控大屏为例 ...

  7. 手把手教你快速使用数据可视化BI软件创建互联网用户数据分析大屏

    灯果数据可视化BI软件是新一代人工智能数据可视化大屏软件,内置丰富的大屏模板,可视化编辑操作,无需任何经验就可以创建属于你自己的大屏.大家可以在他们的官网下载软件.   本文以互联网用户数据分析大屏为 ...

  8. 不懂怎么创建可视化大屏?手把手教你使用数据可视化BI软件创建工厂车间数据监控大屏

    灯果数据可视化BI软件是新一代人工智能数据可视化大屏软件,内置丰富的大屏模板,可视化编辑操作,无需任何经验就可以创建属于你自己的大屏.大家可以在他们的官网下载软件.   本文以工厂车间数据监控大屏为例 ...

  9. 学会这一招,小白也能使用数据可视化BI软件创建医院数据实时展示大屏

    灯果数据可视化BI软件是新一代人工智能数据可视化大屏软件,内置丰富的大屏模板,可视化编辑操作,无需任何经验就可以创建属于你自己的大屏.大家可以在他们的官网下载软件.   本文以医院数据实时展示大屏为例 ...

随机推荐

  1. 【NOI P模拟赛】最短路(树形DP,树的直径)

    题面 给定一棵 n n n 个结点的无根树,每条边的边权均为 1 1 1 . 树上标记有 m m m 个互不相同的关键点,小 A \tt A A 会在这 m m m 个点中等概率随机地选择 k k k ...

  2. 「题解报告」 P3167 [CQOI2014]通配符匹配

    「题解报告」 P3167 [CQOI2014]通配符匹配 思路 *和?显然无法直接匹配,但是可以发现「通配符个数不超过 \(10\) 」,那么我们可以考虑分段匹配. 我们首先把原字符串分成多个以一个通 ...

  3. 记一次 .NET 某金融企业 WPF 程序卡死分析

    一:背景 1. 讲故事 前段时间遇到了一个难度比较高的 dump,经过几个小时的探索,终于给找出来了,在这里做一下整理,希望对大家有所帮助,对自己也是一个总结,好了,老规矩,上 WinDBG 说话. ...

  4. 细数实现全景图VR的几种方式(panorama/cubemap/eac)

    Three.js系列: 在元宇宙看电影,享受 VR 视觉盛宴 Three.js系列: 造个海洋球池来学习物理引擎 Three.js系列: 游戏中的第一.三人称视角 Three.js系列: 数实现全景图 ...

  5. 我的Vue之旅、02 ES6基础、模块、路径、IO

    自定义模块 为什么要模块?模块化源代码能给我们带来什么好处? 试想一个巨无霸网购平台,在没有模块化的情况下,如果出现bug,程序员就要在几百万行代码里调试,导致后期维护成本上升,为了解决问题,模块化按 ...

  6. Django 连接数据库 MySQL

    一.Django 连接 MySQL 修改 settings.py 文件 # 默认用的是sqlite3 # Database # https://docs.djangoproject.com/en/4. ...

  7. Django 使用VScode 创建工程

    一.VSCode 创建Django 工程 VSCode 官方: https://code.visualstudio.com 1 mysite(项目名),创建Django 项目,可以和虚拟环境放在同一目 ...

  8. Logstash集成GaussDB(高斯DB)数据到Elasticsearch

    GaussDB 简介 GaussDB 数据库分为 GaussDB T 和 GaussDB A,分别面向 OLTP 和 OLAP 的业务用户. GaussDB T 数据库是华为公司全自研的分布式数据库, ...

  9. 第六章:Django 综合篇 - 7:网站地图sitemap

    网站地图是根据网站的结构.框架.内容,生成的导航网页,是一个网站所有链接的容器.很多网站的连接层次比较深,蜘蛛很难抓取到,网站地图可以方便搜索引擎或者网络蜘蛛抓取网站页面,了解网站的架构,为网络蜘蛛指 ...

  10. 如何在 Docker 之上使用 Elastic Stack 和 Kafka 可视化公共交通

    文章转载自:https://blog.csdn.net/UbuntuTouch/article/details/106498568 需要掌握的知识点: 1.使用docker-compose方式部署一套 ...