【保姆进阶级】Three.js做一个酷炫的城市展示可视化大屏

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
  • 31
    点赞
  • 114
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ethanpu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值