如何在three.js三维场景里添加echarts图表组件
如果你开发过可视化大屏项目,比如三维数字孪生、智慧城市看板等项目中,经常会碰到一个棘手的问题:怎么在three.js构建的三维场景里,动态展示echarts图表呢?今天,就来和大家详细讲讲,如何利用three.js和ECharts技术的融合,通过自定义拖拽的方式,在three.js三维场景中加载不同的echarts图表组件。
一、借助CSS3DRenderer和CSS3DObject实现融合
three.js提供了一个很实用的API——CSS3DRenderer,它能把DOM元素渲染到3D场景里。不过,使用的时候有些地方得注意:
- CSS3DRenderer渲染的内容,没办法像3D模型材质那样进行导入导出操作。
- 它只支持基础的3D变换,像位移、旋转、缩放这些,像复杂光照、阴影、自定义材质、粒子系统这些高级效果就实现不了。
- 原生支持DOM元素,也就是说,可以直接把HTML、CSS元素,像div、svg,还有ECharts画布当作3D对象来渲染。
二、代码实现过程
(一)封装相关代码
为了让代码结构更清晰,我们把渲染和创建echarts模块的代码,用class类函数封装成css3DRendererModules
。具体代码如下:
export default class css3DRendererModules { css3DRenderer: CSS3DRenderer | null; css3DControls: OrbitControls | null; raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2(); scene: THREE.Scene | null; camera: THREE.PerspectiveCamera | null; renderer: THREE.WebGLRenderer | null; container: HTMLElement | null; constructor() { this.css3DRenderer = null; this.css3DControls = null; this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); this.scene = null; this.camera = null this.renderer = null this.container = document.querySelector('#echarts'); } }
在这段代码里,定义了一些属性,用于存放渲染器、控制器、场景、相机等对象,constructor
构造函数里对这些属性进行了初始化,并获取了页面上id为echarts
的DOM元素。
(二)初始化场景和渲染器
接下来,在init
方法里创建场景、相机、渲染器,并且让CSS3DRenderer渲染器和WebGLRenderer渲染器的位置重叠,同时给相关元素添加pointerEvents
属性,避免影响WebGLRenderer渲染器中场景的交互功能。代码如下:
init() { // 创建场景 this.scene = new THREE.Scene(); const rgbeLoader = new RGBELoader(); const texture = await rgbeLoader.loadAsync('hdr/view-hdr-11.hdr'); texture.mapping = THREE.EquirectangularReflectionMapping; // 创建相机 const { offsetWidth, offsetHeight } = this.container; const aspectRatio = offsetWidth / offsetHeight; this.camera = new THREE.PerspectiveCamera(45, aspectRatio, 1, 20000); this.camera.position.set(0, 2, 6); this.camera.name = 'Camera'; this.camera.updateProjectionMatrix(); // 创建渲染器 this.renderer = new THREE.WebGLRenderer({ antialias: true, // 开启硬件抗锯齿 alpha: true, preserveDrawingBuffer: true, powerPreference: 'high-performance', // 优先使用高性能GPU }); this.renderer.setClearColor(0xcccccc); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 限制最大像素比为2 // 创建css3d渲染器 this.css3DRenderer = new CSS3DRenderer(); this.css3DRenderer.setSize(offsetWidth, offsetHeight); this.css3DRenderer.domElement.style.position = 'absolute'; this.css3DRenderer.domElement.style.pointerEvents = 'none'; this.css3DRenderer.domElement.style.top = '0'; this.css3DRenderer.domElement.style.zIndex = '0'; this.css3DControls = new OrbitControls( this.camera, this.css3DRenderer.domElement ); }
这段代码依次完成了场景、相机、WebGLRenderer渲染器和CSS3DRenderer渲染器的创建,还设置了相机的位置、渲染器的一些属性,并且初始化了用于控制相机视角的OrbitControls
。
(三)创建echarts图表
下面的createEcharts
方法,用来动态创建DOM元素内容,通过Raycaster射线检测和THREE.Vector2()方法获取鼠标在三维场景中的相对位置,根据传入的echarts图表参数信息设置图表数据和类型,再把元素节点转换为three.js可渲染的内容,添加到场景中。代码如下:
/** * 创建echarts * @param options - 选项 */ createEcharts(options: unknown) { const { modelData, clientY, clientX } = options as EchartsType; if (!this?.container || !this?.camera || !this.scene?.children) return; const rect = this?.container.getBoundingClientRect(); this.mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1; this.mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1; this.raycaster.setFromCamera(this.mouse, this?.camera); const intersects = this.raycaster .intersectObjects(this.scene?.children, true) .slice(0, 1); if (intersects.length == 0) return; const element = document.createElement('div'); const tagsMode = createApp({ mounted() { const chartDom = this.$refs.chart as HTMLElement; const myChart = echarts.init(chartDom); myChart.setOption(modelData?.options); }, render() { return ( <div ref="chart" id="echarts" style={{ width: `${modelData.width}px`, height: `${modelData.height}px`, backgroundColor: 'rgba(0,0,0,0.6)', borderRadius: '4px', pointerEvents: 'auto', }} ></div> ); }, }); const vNode = tagsMode.mount(document.createElement('div')); element.appendChild(vNode.$el); const cssObject = new CSS3DObject(element); cssObject.position.set(0, 1.5, 0); cssObject.scale.set(0.004, 0.004, 0.004); const boxGeometry = new THREE.BoxGeometry(3, 0.5, 0.5); const boxMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true, visible: false, }); const helperBox = new THREE.Mesh(boxGeometry, boxMaterial); helperBox.add(cssObject); helperBox.userData = { isTransformControls: true, ...options, }; const { x, y, z } = intersects[0].point; helperBox.position.set(x, y, z); helperBox.name = modelData.name; this.scene?.add(helperBox); }
这里使用了jsx语法,要是想正常使用,得安装@vitejs/plugin-vue-jsx
插件,并且在vite.config.ts
里添加jsx相关配置vueJsx()
。具体配置代码如下:
import { defineConfig, loadEnv } from 'vite'; import vue from '@vitejs/plugin-vue'; import vueJsx from '@vitejs/plugin-vue-jsx'; export default defineConfig((mode) => { return { plugins: [vue(), vueJsx()], resolve: { alias: { '@': resolve(__dirname, 'src'), vue: 'vue/dist/vue.esm-bundler.js', }, }, }; });
(四)实现效果:创建饼图
下面这段代码展示了如何创建一个饼图:
const css3DRendererModules = new css3DRendererModules(); css3DRendererModules.init() const config = { options: { title: { text: '今日访客', left: 'center', textStyle: { color: '#fff', }, }, tooltip: { trigger: 'item', }, legend: { orient: 'vertical', left: 'left', textStyle: { color: '#fff', }, }, series: [ { name: '今日访客', type: 'pie', radius: '50%', data: [ { value: 1048, name: '北京' }, { value: 735, name: '上海' }, { value: 580, name: '广州' }, { value: 484, name: '深圳' }, { value: 300, name: '成都' }, ], emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)', color: '#fff', }, }, }, ], }, height: 500, width: 850, type: 'pie', name: '饼图', } css3DRendererModules.createEcharts(config);
通过上述步骤,一个在three.js三维场景中动态创建echarts图表的功能就实现啦。