3D立体图表展示
前言
偶然间看到优秀的同事设计出新的图表,不仅炫酷,而且还增加了一个展示纬度,感觉不错就打算用babylon实现一版,先上设计稿效果。
一、需求分析
1.1 图表实现
这里抛开非必要开发,直接用成熟的图表库实现出单个图表的效果即可。所以也不算多难,我这里用的是echarts,在官网配置出一个接近的option就可以了。
1.2 图表转化成模型
在三维场景里,至少有两种方法可以实现。
- 先利用图表生成背景透明的图片,再把图片以纹理的形式贴给网格模型。这次用这个。
- 参考CSS3DRenderer.js,原理是生成一个div,通过css3的 transform 属性使用div仿佛在场景中。文献地址
1.3 高亮选中效果
在三维场景里,也有多种效果。
二、核心功能的实现 ( 截取部分代码)
2.1 图表转模型
步骤记录
- 生成图表图片
- 创建合适大小的面片
- 设置材质,主要有:backFaceCulling 双面、hasAlpha纹理透明。
- 设置位置
// 避免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 底座效果
- 用一张图片创建底座的效果,需要比图表的大小大一些。这里只做边缘的辉光效果,创建了一个extrudedPolygon模型,大小刚好是底座面板的边缘,再用GlowLayer增加发光效果。
- 生成一些线,做刻度。
- 生成三维文字,其中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立体图表