原来前端实现科技感的三维地图可以这么简单!!!

1,154 阅读15分钟

在打造数字孪生项目时,我们经常会遇到客户提出的一个特别需求 —— 制作科技感十足的行政区划地图效果。
以往,这种视觉效果的实现可能需要美术人员介入。 但是,您知道吗?其实我们前端开发人员也能轻松应对,而且我们的方法更灵活更易复用
本文将带领一起探索如何使用前端,打造一款酷炫的科技风三维地图。

最终 00_00_00-00_00_30.gif

功能解析

咱一起来探索一下如何实现科技风三维地图这个功能吧~~

数据准备层面

  • 需要准备市级界线、区县级界线、区县界名称geojson

    • 数据下载:使用bigMap、水经维图等软件下载 -> 下载市级界线、区县级界线shp数据;

    • 数据转换:使用qgis、ArcMap等gis软件转换 ->

    1. 将数据转换为投影坐标系【数据转换参考推文: 包学会!轻松掌握坐标系转换技巧!】;
    2. 将转换的shp格式数据导出为geojson格式数据; image
    3. 使用qgis等gis软件提取区县级行政边界的中心点坐标以及对应的名称,并将其数据进行导出; image

前端实现层面

前端实现科技风地图这里我拆解成下面几个步骤进行实现:

11 00_00_00-00_00_30.gif

  1. 样式调整。
调整前 00_00_00-00_00_30.gif 2. 行政区划。 行政区划 3. 立体。 2 00_00_00-00_00_30.gif 4. 科技风。 3 00_00_00-00_00_30.gif 这里提前准备好了对应的工程以及代码,大家可以下载下来实践操作一下哟~

专题资源链接(包含数据、代码)下载: ttps://pan.baidu.com/s/1V9mX2rrByeQ4M7ZAJIaDfA?pwd=rckt

该代码为html。需要将lib/aircity的SDK替换成本地Cloud SDK,同时,由于需要读取 geojson 文件,需启动相应的 Web 服务以确保文件访问。

  1. 替换SDK步骤:Cloud SDK获取方式:将SDK文件夹的ac.min.js进行复制; image
  2. 将代码/行政区划/lib/aircity的ac.min.js替换成SDK的ac.min.js。 image
  3. 在编辑器中,通过启动 Web 服务进行访问html,有两种方式:一种是使用右键的 “Open with Live Server” 选项,另一种是点击编辑器底部的 “Go Live” 按钮。 image

实现步骤

总体分析

打造一个炫酷且充满科技感的地图效果,需要挑选对应色彩主题。在蓝色、绿色、紫色这三种科技风格的代表色中,以蓝色为例作为地图的主色调,同时以浅蓝色和银色作为辅助色彩,打造一个充满科技感和现代感的视觉体验。 3 00_00_00-00_00_30.gif 这里我从样式调整、行政区划、立体、科技风效果一一进行讲解让大家查看到对应的变化。
(ps: 每小节讲解功能部分的核心代码,非最终整合代码;最终完整代码可参考百度网盘链接)

样式调整

为什么会有样式调整的这一个步骤呢?

11 00_00_00-00_00_30.gif

在初始状态下,大气层散射的存在会对行政区划颜色效果造成色差,因此建议开启黑暗模式,关闭大气层与云层以减少视觉干扰;由于,图层亮度比较高,在后续的展示可能过于突出,故需对图层亮度进行调整,以便更好地突出行政区划面地展示效果。

我们需要做的操作:

  • 使用fdapi.weather.setDarkMode(true),开启黑暗模式;

  • 将图层的亮度调低。

    • 遍历图层树,后续我们通过图层名进行拾取到对应的图层id,使用此方法可读性较高,后续也易于维护;
    • 使用fdapi.tileLayer.setStyle()对图层进行设置亮度。
// 图层树对象
const infoTreeObj = {}
const setStyles = async () => {
  //样式调整:a.打开黑暗模式  b.调整地形影像的亮度
  // 开启黑暗模式
  await fdapi.weather.setDarkMode(true)
  // 获取图层树
  const { infotree } = await fdapi.infoTree.get()
  // 遍历图层树,根据图层名获取图层id
  infotree.forEach(item => {
    infoTreeObj[item.name] = item.iD
  })
 
  // 图层亮度调整
  await fdapi.tileLayer.setStyle(
    infoTreeObj['大地形影像'],
    null,
    null,
    null,
    0.4
  )
}
行政区划

在一般情况下,绘制市行政区划图时,常采用区县数据进行绘制面,并用线来区分不同区域,使用点来标识区域所属。接下来,我们将共同探讨如何实现标准的行政区划效果。
我们需要做的操作:

  1. 需要提前获取准备的数据,用于后续线与面的添加。在html中需要以fetch获取数据,在Vue框架中可以使用axios获取数据。
  • 此数据在项目初始化的时候进行获取。
function initScene() {
  fetch(getCountyDataPath)
    .then(res => res.json())
    .then(jsonData => {
      // 处理JSON数据
      getCountyData = JSON.parse(JSON.stringify(jsonData))
    })
    .catch(err => {
      console.error('Error:', err)
    })
    new DigitalTwinPlayer(HostConfig.Player, {
    domId'player',
    apiOptions: {
      onReadyasync () => {
        fdapi.reset(1 | 2 | 4)
      }
    }
  })

2. 使用Polygon将读取区县数据进行绘制面。面的样式的选择可以参考推文:前端三维可视化线面样式选择大揭秘!!!

行政区划面

行政区划面

  • 全局需要创建一个图层对象,便于后续对图层进行统一管理;
// 图层ID对象
const LayerIdObj = {
  polyline: [],
  polygon3d: [],
  polygon: []
}
  • 以科技风为主题,以蓝色为主色调,这里面的样式style采用的是0单色样式
// 创建数组存放polygon id
const polygonList = []
// 遍历县级数据
getCountyData.features.forEach((item, index) => {
const coordinates = item.geometry.coordinates[0][0]
const city_polygon_id = 'city_polygon_' + index
LayerIdObj.polygon.push(city_polygon_id)
const city_polygon = {
  id: city_polygon_id,
  coordinates: coordinates.map(coord => [coord[0], coord[1]]),
  coordinateType0//坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
  range: [110000000], //可视范围:[近裁距离, 远裁距离],取值范围: [任意负值, 任意正值]
  color: [5 / 25550 / 25574 / 2550.8], //多边形的填充颜色
  intensity20//亮度
  style0//单色 请参照API开发文档选取枚举
  depthTestfalse//是否做深度检测 开启后会被地形高度遮挡
  priority1 //显示优先级 值越大显示越靠上
}
polygonList.push(city_polygon)
})
// 进行添加polygon
await fdapi.polygon.add(polygonList)

3. 使用Polyline线来区分每个区县区域,同样,线的样式选择参考推文:前端三维可视化线面样式选择大揭秘!!!

行政区划线面

行政区划线面

  • 遍历区县数据,每个区域线唯一id值,将其推送push至数组中,以存储Polyline的ID;
  • 在科技风格的基础上,线颜色以银色作为辅助色彩,为后续营造一个充满科技感的视觉效果;
  • 适当降低Polyline的流速flowRate,以优化显示效果;
  • 采用的线条样式style为静态的样式4-正常线条;
  • 使用fdapi.polyline.add()一次性添加所有Polyline,以提高效率和统一性。
// 创建数组存放polyline id
const polylineList = []
getCountyData.features.forEach((item, index) => {
// 获取坐标
const coordinates = item.geometry.coordinates[0][0]
const county_border_id = 'county_border_' + index
LayerIdObj.polyline.push(county_border_id)
const county_border = {
  id: county_border_id, //折线唯一标识id
  coordinates: coordinates.map(coord => [coord[0], coord[1]]),
  coordinateType0//坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
  range: [110000000], //可视范围:[近裁距离, 远裁距离],取值范围: [任意负值, 任意正值]
  color: [1111], //折线颜色
  thickness150//折线宽度
  intensity10//亮度
  flowRate0.1//流速
  shape0//折线类型 0:直线, 1:曲线
  depthTestfalse//是否做深度检测 开启后会被地形高度遮挡
  style4//折线样式 参考样式枚举:PolylineStyle
  tiling0.5 //材质贴图平铺比例
}
polylineList.push(county_border)
})
await fdapi.polyline.add(polylineList)    

4. 凸显整个行政区划效果,给行政区划外轮廓添加线。

  • 外轮廓Polyline 的线宽thickness应大于内轮廓 Polyline(区县Polyline),突出外轮廓。

添加外轮廓

// 创建边框线
const border_coordinates =
  getCityData.features[0].geometry.coordinates[0][0]
const city_border = {
  id: 'city_border', //折线唯一标识id
  coordinates: border_coordinates.map(coord => [coord[0], coord[1]]),
  coordinateType: 0, //坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
  range: [1, 10000000], //可视范围:[近裁距离, 远裁距离],取值范围: [任意负值, 任意正值]
  color: [1111], //折线颜色
  thickness300//折线宽度
  intensity10//亮度
  flowRate0.1//流速
  shape0//折线类型 0:直线, 1:曲线
  depthTest: false, //是否做深度检测 开启后会被地形高度遮挡
  style4//折线样式 参考样式枚举:PolylineStyle
  tiling0.5 //材质贴图平铺比例
}
await fdapi.polyline.add(city_border)
  • 通过添加光流样式(style:3)的 Polyline 进一步强化外轮廓效果。

光流线 00_00_00-00_00_30.gif

const city_border_dynamic = {
  id: 'city_border_dynamic'//折线唯一标识id
  coordinates: border_coordinates.map(coord => [coord[0], coord[1]]),
  coordinateType: 0//坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
  range: [110000000], //可视范围:[近裁距离, 远裁距离],取值范围: [任意负值, 任意正值]
  color: [1111], //折线颜色
  thickness: 600//折线宽度
  intensity: 10//亮度
  flowRate: 0.3//流速
  shape: 0//折线类型 0:直线, 1:曲线
  depthTest: false//是否做深度检测 开启后会被地形高度遮挡
  style: 3//折线样式 参考样式枚举:PolylineStyle
  tiling: 2 //材质贴图平铺比例
}
await fdapi.polyline.add(city_border_dynamic)

5. 添加完线面,那我们就需要加上我们的区县名称来标识我们区县区域所属。点的样式添加可参考推文:震惊!三维可视化前端竟能让二维标签玩出 7 种效果欸!

加点 00_00_00-00_00_30.gif

  • 需要获取区域名称geojson数据,同样是在初始化时进行获取。
// 获取行政县界名称
const CountyLabelPath = './lib/data/4549苏州县界_名称标注.geojson'
let CountyLabelData = '' // 行政区县界名称数据存储
function initScene() {
fetch(CountyLabelPath)
  .then(res => res.json())
  .then(jsonData => {
    // 处理JSON数据
    getCountyLabelData = JSON.parse(JSON.stringify(jsonData))
  })
  .catch(err => {
    console.error('Error:', err)
  })
}
  • 遍历区域名称数据,每个区域名称需要唯一id值,将其推送push至数组中,以存储Marker
  • 名称通过遍历数据进行获取,并将其填入到属性text中;
  • 这里字的展示,用的普通文字点位;
  • 文字可视距离的远裁距离设置大些,拉远可以查看到点标注。
// 创建数组存放marker id
const markerList = []
// 遍历点数据
getCountyLabelData.features.forEach((item, index) => {
    // 获取坐标
  const coordinates = item.geometry.coordinates
    // 获取名称
  const name = item.properties.Name
  let label = {
    id'county_label_' + index,
    groupId'county_label',
    coordinate: [...coordinates], //坐标位置
    coordinateType0//默认0是投影坐标系,也可以设置为经纬度空间坐标系值为1
    range: [1100000000], //可视范围
    viewHeightRange: [1100000000], // 可见高度范围

    text: name, //显示的文字
    useTextAnimationtrue//关闭文字展开动画效果 打开会影响效率
    textRange: [1100000000], //文本可视范围[近裁距离, 远裁距离]
    textBackgroundColor: [0000], //文本背景颜色
    fontSize16//字体大小
    fontOutlineSize2//字体轮廓线大小
    fontColorColor.White//字体颜色
    fontOutlineColorColor.Black//字体轮廓线颜色

    displayMode2 //智能显示模式  开发过程中请根据业务需求判断使用四种显示模式
  }
  markerList.push(label)
})
// 添加行政名称
await fdapi.marker.add(markerList)
立体效果

实现完基础行政区划,接下来我们需要进行升级啦~~~不再是只有美术可以实现立体效果,咱前端同样也可以实现了~

polygon3d 00_00_00-00_00_30.gif 这一部分我们讲解一下如何实现行政区划立体效果
当前的行政区划紧贴地形呈现平面效果,我们要让它立体呈现出来。 需要做的操作:

  1. 将行政区划进行抬升,给与高度height
  • 设置一个全局变量height,给之前创建的点线面的coordinates的z值进行赋值height -> 参考:coordinates: coordinates.map(coord => [coord[0], coord[1], height])
// 高度
const height = 10000

2. 给行政区划添加侧面,呈现立体效果。

  • 使用Polygon3d进行添加侧面,需关闭Polygon3d的顶面和侧面显示。
  • Polygon3d可采用样式2:渐变静态进行呈现。
  • 渐变侧面颜色和行政区划线颜色一致。
  • 要实现Polygon3d侧面朝下的效果,需将height设置为负值
// 创建边框数组存放polygon3d id
const WallList = []

getCountyData.features.forEach((item, index) => {
  const coordinates = item.geometry.coordinates[0][0]
  const county_wall_id = 'county_wall_' + index
  LayerIdObj.polygon3d.push(county_wall_id)
  let county_wall = {
    id: county_wall_id,
    coordinates: coordinates.map(coord => [coord[0], coord[1], height]),
    coordinateType0//坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
    color'#ffffff'//颜色值
    height: -height * 0.5//3D多边形的高度
    intensity1.0//亮度
    style2//3DPolygon的样式 请参照API开发文档选取枚举
    tillingX0//可选,仅当3DPolygon的样式支持贴图显示,贴图横向平铺
    tillingY0//可选,仅当3DPolygon的样式支持贴图显示,贴图纵向平铺
    generateTop: false, //是否生成顶面
    generateSide: true, //是否生成侧面
    generateBottom: false //是否生成底面
  }
  WallList.push(county_wall)
})

fdapi.polygon3d.add(WallList)
科技风

成功实现立体效果后,接下来的步骤是采用蓝色作为主色调,对整个行政区划进行点缀,以此塑造科技风格的视觉效果。

  1. 为突出行政区划的科技风效果,采用Polygon3d,添加蓝色色调的行政区划侧面,以增强视觉效果的立体感和科技感。

蓝色polygon3d 00_00_00-00_00_30.gif

  • 关闭 Polygon3d 的顶面和底面显示功能。
  • 采用Polygon3d动态渐变样式,突出科技感效果。
  • Polygon3d 的高度值需设为负值,以实现侧面朝下的效果。
const coordinates = getCityData.features[0].geometry.coordinates[0][0]
let city_wall = {
 id: 'city_wall',
 coordinates: coordinates.map(coord => [coord[0], coord[1], height]),
 coordinateType0//坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
 color'#215cca'//颜色值
 height: -height * 1.5//3D多边形的高度
 intensity1.0//亮度
 style4//3DPolygon的样式 请参照API开发文档选取枚举
 tillingX0//可选,仅当3DPolygon的样式支持贴图显示,贴图横向平铺
 tillingY0//可选,仅当3DPolygon的样式支持贴图显示,贴图纵向平铺
 generateTop: false, //是否生成顶面
 generateSide: true, //是否生成侧面
 generateBottom: false //是否生成底面
}
fdapi.polygon3d.add(city_wall)

2. 通过自定义样式的 Polygon 添加顶面,以提升科技风格效果。自定义Polygon样式设置参考推文Polygon3d自定义样式模块:前端三维可视化线面样式选择大揭秘!!!

闪面polygon 00_00_00-00_00_30.gif

  • 需要使用到区县geojson数据。
  • 采用平面混合_2自定义样式。
  • 根据实际情况调整自定义样式Polygon的参数,特别是UV相关参数,以调整纹理密度和速度,达到更真实的效果。
  • 自定义材质颜色设置为蓝色色调。
let polygonList = []
getCountyData.features.forEach((item, index) => {
  // 获取坐标
  const coordinates = item.geometry.coordinates[0][0]
  const city_polygon_dynamic_id = 'city_polygon_dynamic' + index
  LayerIdObj.polygon.push(city_polygon_dynamic_id)
  const city_polygon_dynamic = {
    id: city_polygon_dynamic_id,
    coordinates: coordinates.map(coord => [coord[0], coord[1], height]),
    coordinateType0//坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
    range: [110000000], //可视范围:[近裁距离, 远裁距离],取值范围: [任意负值, 任意正值]
    intensity1//亮度
    depthTestfalse//是否做深度检测 开启后会被地形高度遮挡
    priority1//显示优先级 值越大显示越靠上
    material'/JC_CustomAssets/PolygonLibrary/Exhibition/平面混合_2'//自定义材质路径 设置后style相关参数会失效
    scalarParameters: [
      { name'亮条U速度'value0.1 },
      { name'亮条V速度'value0.1 },
      { name'亮条UV'value0.25 },
      { name'亮条亮度'value10 },
      { name'亮条旋转'value0.33 },
      { name'亮度'value2 },
      { name'UV缩放'value0.001 },
      { name'不透明度'value0.01 }
    ], //材质数值类型参数
    vectorParameters: [
      { name'颜色'value: [19 / 255204 / 255255 / 255] }
    ] //材质数组类型参数
  }
  polygonList.push(city_polygon_dynamic)
})
fdapi.polygon.add(polygonList)

3. 添加自定义面之后,由于最开始有个行政区划面并且高度一致,就会出现闪面。需要将底下面的高度-1,解决闪面现象。

最终 00_00_00-00_00_30.gif

const polygonList = []
// 遍历县级数据
getCountyData.features.forEach((item, index) => {
const coordinates = item.geometry.coordinates[0][0]
const city_polygon_id = 'city_polygon_' + index
LayerIdObj.polygon.push(city_polygon_id)
const city_polygon = {
  id: city_polygon_id,
  coordinates: coordinates.map(coord => [coord[0], coord[1],height-1]),
  coordinateType0//坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
  range: [110000000], //可视范围:[近裁距离, 远裁距离],取值范围: [任意负值, 任意正值]
  color: [5 / 25550 / 25574 / 2550.8], //多边形的填充颜色
  intensity20//亮度
  style0//单色 请参照API开发文档选取枚举
  depthTestfalse//是否做深度检测 开启后会被地形高度遮挡
  priority1 //显示优先级 值越大显示越靠上
}
polygonList.push(city_polygon)
})
// 进行添加polygon
await fdapi.polygon.add(polygonList)

总结

本教程带着大家从零起步,逐步掌握前端实现科技风地图效果。通过学习相应的流程和操作方法,会发现前端实现科技风地图并非难事。希望本文能给大家带来帮助。鼓励大家动手实践起来~~也欢迎大家一起探讨实现效果的创新以及突破!!!

最终 00_00_00-00_00_30.gif

专题资源链接(包含数据、代码)下载: ttps://pan.baidu.com/s/1V9mX2rrByeQ4M7ZAJIaDfA?pwd=rckt

这里准备了两份html,内容是一致的。一个是按照推文流程去实现的,大家可以根据流程去了解每一个步骤;另一个将相同的数据请求集中处理,便于未来的维护和优化工作。