章
目
录
如果你开发过可视化大屏项目,比如三维数字孪生、智慧城市看板等项目中,经常会碰到一个棘手的问题:怎么在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图表的功能就实现啦。