hi,大家好,我是ethan。
想记录博客很久了,一直懒得开个头,以前写过全栈、java、写过python、写过前端,写过安全、写过互联网,但是我还是更喜欢前端可视化,平时也喜欢研究一下可视化的技术,也是从d3、gis、threejs、echarts、hicharts、cesium一步步淌过来的,可视化方向的路还有很长,我觉得一些shader实在是好难....
web3.0盛行,元宇宙也是跟前端密切相关的,也想学习一下unity、three.ar.js之类的,有想法的小伙伴可以一起沟通一下~
言归正传,最近呢在做一个可视化大屏,当然要炫,毕竟领导喜欢,废话不多说,先上预览:
bb185a2e-b902-48eb-91a6-5ea79eaf53c9
分解代码前,我们先介绍一些这里面有几个技术点:
1、d3.js通过投影把地图数据的json映射到3维空间中,城市地图的json下载我就不多讲了,网上有很多教程,换成自己所需的城市就行;2、地图上展示的数据展示的label,一开始用的sprite小精灵模型做的,但是会失真不清楚,后来换成了CSS2DRenderer这种方式,就相当于把html渲染到3维空间里,屡试不爽;
3、为了达到“酷炫智能”效果,在一加载和点击区县的时候,做了camera的动画(镜头移动、拉近),在这里就要在vue中引入tween.js了,tween做补间动画,还是很好用的;
4、地图边缘做了个流光效果,这个有很多厉害的博主介绍过,我是稍作了下修改;
5、每切换一个tab,隐藏/显示相应模型,所以把一组模型放到一组group里;
接下来我们可以带着上面几个点,看代码~!
项目使用vue的框架,我们先来看看项目目录、依赖都有哪些,其中引入elementUI就是为了用用里面的按钮,不用自己写了:
(Menu.vue是测试了一个3D的菜单,跟此项目没有关联,可以先不用理会)
{
"name": "default",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"@tweenjs/tween.js": "^18.6.4",
"core-js": "^2.6.5",
"element-ui": "^2.15.8",
"three": "^0.140.2",
"vue": "^2.6.10"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-service": "^3.8.0",
"d3": "^7.4.4"
}
}
tween这个包不好在vue里面直接用,所以提前去下载好,然后还要在main.js里面做声明
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
// 补间动画
import tween from "./utils/tween";
Vue.use(ElementUI);
Vue.use(tween);
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
接下来,我们看一下主要的代码Main.vue
<template>
<div>
<div id="container"></div>
<div id="tooltip"></div>
<el-button-group class="button-group">
<el-button type="" icon="" @click="groupOneChange">首页总览</el-button>
<el-button type="" icon="" @click="groupTwoChange">应急管理</el-button>
<el-button type="" icon="" @click="groupThreeChange">能源管理</el-button>
<el-button type="" icon="" @click="groupFourChange">环境监测</el-button>
<!-- <el-button type="" icon="">综合能源监控中心</el-button> -->
</el-button-group>
</div>
</template>
其中:
container块是主要渲染3d画布的div;
tooltip是鼠标悬浮到区县时显示区县名称div;
button-group是左上部分做tab切换的按钮组(全篇引入了elementUI就在这用到了...)
这是需要的组件,提前引入
import * as THREE from "three";
import * as d3 from 'd3';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
下面是放在data里的属性,把摄像机、场景、控制器、城市上的数据、城市上的模型,都放在这先声明一下,因为牵扯到很多模型、摄像机、动画的逻辑变化,所以放到这就相当于全局变量,后续用的话都很方便。
data() {
return {
camera: null,
scene: null,
renderer: null,
labelRenderer: null,
container: null,
// mesh: null,
controller: null,
map: null,
raycaster: null,
mouse: null,
tooltip: null,
lastPick: null,
mapEdgeLightObj: {
mapEdgePoints: [],
lightOpacityGeometry: null, // 单独把geometry提出来,动画用
// 边缘流光参数
lightSpeed: 3,
lightCurrentPos: 0,
lightOpacitys: null,
},
// 每个屏幕模型一组
groupOne: new THREE.Group(),
groupTwo: new THREE.Group(),
groupThree: new THREE.Group(),
groupFour: new THREE.Group(),
// groupOne 统计信息
cityWaveMeshArr: [],
cityCylinderMeshArr: [],
cityMarkerMeshArr: [],
cityNumMeshArr: [],
// groupTwo 告警信息
alarmWaveMeshArr: [],
alarmCylinderMeshArr: [],
alarmNameMeshArr: [],
// groupThree 能源
energyWaveMeshArr: [],
energyCylinderMeshArr: [],
energyNameMeshArr: [],
// groupFour 环境
monitorWaveMeshArr: [],
monitorIconMeshArr: [],
monitorNameMeshArr: [],
// 城市信息
mapConfig: {
deep: 0.2,
},
// 摄像机移动位置,初始:0, -5, 1
cameraPosArr: [
// {x: 0.0, y: -0.3, z: 1},
// {x: 5.0, y: 5.0, z: 2},
// {x: 3.0, y: 3.0, z: 2},
// {x: 0, y: 5.0, z: 2},
// {x: -2.0, y: 3.0, z: 1},
{x: 0, y: -3.0, z: 3.8},
],
// 数据 - 区县总数量
dataTotal: [xxxxxx],
dataAlarm: [xxxxxx],
dataEnergy: [xxxxxx],
dataMonitor: [xxxxxx],
};
},
mounted函数不多说了,初始化什么的都放在这
mounted() {
this.init();
this.animate();
window.addEventListener('resize', this.onWindowSize)
},
着重看一下methods里面的方法,首先是把three的几大基本元素初始化了
//初始化
init() {
this.container = document.getElementById("container");
this.setScene();
this.setCamera();
this.setRenderer(); // 创建渲染器对象
this.setController(); // 创建控件对象
this.addHelper();
this.loadMapData();
this.setEarth();
this.setRaycaster();
this.setLight();
},
setScene() {
// 创建场景对象Scene
this.scene = new THREE.Scene();
},
setCamera() {
// 第二参数就是 长度和宽度比 默认采用浏览器 返回以像素为单位的窗口的内部宽度和高度
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
500
);
this.camera.position.set(0, -5, 1); // 0, -5, 1
this.camera.lookAt(new THREE.Vector3(0, 0, 0)); // 0, 0, 0 this.scene.position
},
setRenderer() {
this.renderer = new THREE.WebGLRenderer({
antialias: true,
// logarithmicDepthBuffer: true, // 是否使用对数深度缓存
});
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
// this.renderer.sortObjects = false; // 是否需要对对象排序
this.container.appendChild(this.renderer.domElement);
this.labelRenderer = new CSS2DRenderer();
this.labelRenderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.labelRenderer.domElement.style.position = 'absolute';
this.labelRenderer.domElement.style.top = 0;
this.container.appendChild(this.labelRenderer.domElement);
},
setController() {
this.controller = new OrbitControls(this.camera, this.labelRenderer.domElement);
this.controller.minDistance = 2;
this.controller.maxDistance = 5.5 // 5.5
// 阻尼(惯性)
// this.controller.enableDamping = true;
// this.controller.dampingFactor = 0.04;
this.controller.minAzimuthAngle = -Math.PI / 4;
this.controller.maxAzimuthAngle = Math.PI / 4;
this.controller.minPolarAngle = 1;
this.controller.maxPolarAngle = Math.PI - 0.1;
// 修改相机的lookAt是不会影响THREE.OrbitControls的target的
// this.controller.target = new THREE.Vector3(0, -5, 2);
},
// 辅助线
addHelper() {
// let helpe