首页IT科技threejs引入3d模型(threejs三维地图大屏项目分享)

threejs引入3d模型(threejs三维地图大屏项目分享)

时间2025-05-01 09:27:23分类IT科技浏览3991
导读:这是最近公司的一个项目。客户的需求是基于总公司和子公司的数据,开发一个数据展示大屏。 大屏两边都是一些图表展示数据,中间部分是一个三维中国地图,点击中国地图的某个省份,可以下钻到省份地图的展示。 地图上,会做一些数据的标注,信息标牌。 如下图所示:...

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

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

2d图表

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

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

三维地图的展示

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

建模人员对地图部分进行建模 通过json数据生成三维模型 通过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坐标的高度进行颜色的渐变差值运算                 。

三维地图的贴图

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j7BuKd9p-1667965040240)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5f0c6a260f3647b991609a440ae85002~tplv-k3u1fbpfcp-watermark.image?)]

这需要我们的贴图来进行解决           。 但是贴图并不简单   ,涉及到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

其他

其他方面包括

点击省份下钻 技术实现就是隐藏其他省份模型         ,显示当前省份模型                ,并加载当前省份的点位数据               。技术思路比较简单。 鼠标悬浮显示名称等信息 通过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彪叔           ” 可以添加作者微信进行交流               ,及时收到更多有价值的文章                。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
Ubuntu安装输入法(史上最简单 OpenCV for C++ 在 Windows 和 Ubuntu 上编译安装使用教程) 域名未通过注册局审批怎么办(未注册老域名挖掘教程-未注册域名扫描软件)