【Babylon提升】结合echarts实现3D立体图表展示


前言

偶然间看到优秀的同事设计出新的图表,不仅炫酷,而且还增加了一个展示纬度,感觉不错就打算用babylon实现一版,先上设计稿效果。

一、需求分析

1.1 图表实现

这里抛开非必要开发,直接用成熟的图表库实现出单个图表的效果即可。所以也不算多难,我这里用的是echarts,在官网配置出一个接近的option就可以了。

1.2 图表转化成模型

在三维场景里,至少有两种方法可以实现。

  • 先利用图表生成背景透明的图片,再把图片以纹理的形式贴给网格模型。这次用这个。
  • 参考CSS3DRenderer.js,原理是生成一个div,通过css3的 transform 属性使用div仿佛在场景中。文献地址

1.3 高亮选中效果

在三维场景里,也有多种效果。

二、核心功能的实现 ( 截取部分代码)

2.1 图表转模型

步骤记录

  1. 生成图表图片
  2. 创建合适大小的面片
  3. 设置材质,主要有:backFaceCulling 双面、hasAlpha纹理透明。
  4. 设置位置
// 避免finished事件多次触发, 不知道为什么finished会多次触发
let flag = false
// 基于准备好的dom,初始化echarts实例
const myChart = echarts.init(chartDiv)
// 绘制图表 option包含数据
myChart.setOption(option)
myChart.on('finished', () => {
  if (flag) {
    // 避免多次
    return 
  }
  flag = true
  // 1、生成图片
  const imgUrl:string = myChart.getDataURL({
    type: 'png',
    pixelRatio: 2,
    backgroundColor: 'rgba(128, 128, 128, 0)'
  })
  
  // 2、创建合适大小的面片
  let plane = BABYLON.MeshBuilder.CreatePlane("plane", {width: this.size, height: this.chartsHeight},this.scene)
  plane.parent = this.chartsNode
  // 创建材质并设置为双面
  const material = new BABYLON.StandardMaterial("material")
  material.backFaceCulling = false // 关闭背面剪切,使材质双面
  // 2.1 创建纹理并加载Base64编码的图片数据
  const texture = new BABYLON.Texture(imgUrl) // 替换为实际的Base64编码字符串  
  texture.hasAlpha = true // 如果纹理有透明度,设置为true

  // 2.2 存储,用于刷新
  this.chartsTextureMap[index] = texture
  // 应用纹理到材质
  material.diffuseTexture = texture
  // 纹理的包装模式(wrapping mode)决定了纹理在超过其原始尺寸时的处理方式。对于不需要纹理重复的场景,可以将包装模式设置为 CLAMP_ADDRESSMODE
  material.diffuseTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE
  material.diffuseTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE
  // 取消光照影响
  material.emissiveColor = BABYLON.Color3.White()
  plane.material = material
  // 抬高到基准面
  plane.position.y = this.chartsHeight / 2
  callback(plane, index)

  // 3、注册点击事件
  // 创建ActionManager并将其添加到Mesh
  plane.actionManager = new BABYLON.ActionManager(this.scene);

  // 注册点击触发器和动作
  plane.actionManager.registerAction(
    new BABYLON.ExecuteCodeAction(
      BABYLON.ActionManager.OnLeftPickTrigger,
      function (evt) {
        // 在这里添加点击后的代码
        clickCallback(index, plane)
      }
    )
  );
})

2.2 底座效果

  1. 用一张图片创建底座的效果,需要比图表的大小大一些。这里只做边缘的辉光效果,创建了一个extrudedPolygon模型,大小刚好是底座面板的边缘,再用GlowLayer增加发光效果。
  2. 生成一些线,做刻度。
  3. 生成三维文字,其中this.font 需要通过一个在线转化工具把自己的字体转化成可用的json文件。转化工具
/**
 * 绘制底座
 * @param xName 横轴名称 例如:['02:00', '04:00', '06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '24:00']
 * @param yName 纵轴名称 例如:['西兴路-滨康路', '西兴路-滨康路', '西兴路-滨康路', '西兴路-滨康路', '西兴路-滨康路', '西兴路-滨康路', '西兴路-滨康路']
 */
drawChartBase (xName: string[], yName: string[]) : void {
  const xLen = xName.length
  
  const baseSize = this.size / (xLen - 1) * (xLen + 1)
  // 1、创建底面
  let plane = BABYLON.MeshBuilder.CreatePlane("basePlane", {width: baseSize, height: baseSize},this.scene);
  plane.parent = this.chartBaseNode

  const material = new BABYLON.StandardMaterial("material");
  const texture = new BABYLON.Texture(this.chartBaseBg) // 替换为实际的Base64编码字符串  
  texture.hasAlpha = true // 如果纹理有透明度,设置为true
  // 应用纹理到材质
  material.diffuseTexture = texture
  material.backFaceCulling = false; // 关闭背面剪切,使材质双面
  // 取消光照影响
  material.specularColor = BABYLON.Color3.Black()
  // 发光颜色值
  const color = BABYLON.Color3.FromInts(
    (0x8DF885 >> 16) & 255,
    (0x8DF885 >> 8) & 255,
    0x18DF885 & 255
  )
  plane.material = material
  plane.rotation.x = Math.PI / 2

  // 2、生成正方形边,用于边缘发光效果
  const shape = [[-baseSize/2, -baseSize/2],[baseSize/2, -baseSize/2],[baseSize/2, baseSize/2],[-baseSize/2, baseSize/2]].map((item)=>{
    return new BABYLON.Vector3(item[0], 0, item[1])
  })
  const holes = [[-baseSize/2, -baseSize/2],[baseSize/2, -baseSize/2],[baseSize/2, baseSize/2],[-baseSize/2, baseSize/2]].map((item)=>{
    return new BABYLON.Vector3(item[0] * 0.99, 0, item[1] * 0.99)
  })
  const extrudedPolygon = BABYLON.MeshBuilder.ExtrudePolygon('polygon', {
    shape, 
    holes:[holes],
    depth: 0.01, 
    sideOrientation: BABYLON.Mesh.DOUBLESIDE 
  }, this.scene, earcut)
  extrudedPolygon.parent = this.chartBaseNode
  extrudedPolygon.material = material
  // 发光颜色
  material.emissiveColor = color

  extrudedPolygon.freezeWorldMatrix()
  extrudedPolygon.convertToUnIndexedMesh()
  
  // 2.1 加入辉光图层,只对边框发光
  this.glowLayer.addIncludedOnlyMesh(extrudedPolygon)

  // 3、划刻度线
  const step = baseSize / (xName.length + 1)
  for (let index = 0; index < xName.length; index++) {
    var lines = BABYLON.MeshBuilder.CreateLines("lines", { 
      points: [
        new BABYLON.Vector3(-baseSize/2 + step * (index + 1), 0, baseSize/2), 
        new BABYLON.Vector3(-baseSize/2 + step * (index + 1), 0, -baseSize/2)
      ]
    }, this.scene);
    lines.color = new BABYLON.Color3(1, 1, 1);
    // 3.1 x轴名称
    const textMesh = this.createText(xName[index], new BABYLON.Vector3(-baseSize/2 + step * (index + 1), 0, -baseSize/2 - step), true)
    lines.parent = this.chartBaseNode
    if(textMesh) {
      textMesh.parent = this.chartBaseNode
    }
  }

  const minZ = - this.size / 2
  const maxZ = this.size / 2
  for (let index = 0; index < yName.length; index++) {
    // 3.2 y轴名称
    const textMesh = this.createText(yName[index], new BABYLON.Vector3(baseSize / 2 + step, 0, minZ + ( maxZ - minZ) * index / (yName.length - 1)), false)
    if(textMesh) {
      textMesh.parent = this.chartBaseNode
    }
  }
}
/**
 * 创建横纵文字
 * @param content 文本
 * @param position 显示位置
 * @param mark 控制位置,true  横轴, false 纵轴
 * @returns 
 */
createText (content: string, position: BABYLON.Vector3, mark: Boolean = true) :BABYLON.Nullable<BABYLON.Mesh>{
  const text = BABYLON.MeshBuilder.CreateText(
    "text",
    content,
    this.font,
    {
      size: 0.1,
      resolution: 64,
      depth: 0.01,
    },
    this.scene,
    earcut
  );
  if (text) {
    text.rotation.x = Math.PI / 2
    text.rotation.y = mark ? -Math.PI / 2 : 0
    text.position.copyFrom(position)
  }
  return text
}

2.3 选中效果

在2.1中已经实现mesh的点击事件,这里只摘取高亮的效果实现。

this.highlightLayer = new BABYLON.HighlightLayer("hl1", scene);

// 清除其他
this.highlightLayer.removeAllMeshes()
// 增加高亮mesh,高亮颜色为白色
this.highlightLayer.addMesh(mesh, BABYLON.Color3.White());

2.4 刷新指定图表

// 拿到提前存储的texture
const texture = this.chartsTextureMap[index]

// 拿到生成的图片进行更新。
texture.updateURL(imgUrl)

2.5 增加一点动画效果,呼吸灯

// 边框呼吸灯效果
openAnimate () :void{
  let lastTime: number
  const animate = (time:number) => {
    if (!lastTime) {
      lastTime = time
    } else {
      const diff: number = (time - lastTime) * 0.1
      this.glowLayer.intensity = Math.sin(Math.PI / 180 * diff) / 2 + 1.5 / 2  // [0.5 - 1.5]
    }
    requestAnimationFrame(animate)
  }
  requestAnimationFrame(animate)
}

三、成品展示

3D立体图表

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值