vue+three.js实现模型发光、视角改变、相机移动、管道流动动画

前端小白正在学习three.js,有感兴趣的大家一起相互交流
代码主要包括模型发光、3D卡片、视角转换、管道流动等在数字孪生、智慧工厂中常用的一些方法
我这里用的三个box,在实际应用中多为obj+mtl格式、或者gltf的模型,这些方法也是一样通用的
vue+three.js实现模型发光、视角改变、相机移动、管道流动动画_第1张图片

<template>
  <div class="about">
    <div id="model">
      <div id="canvas1" style="display: none">canvas1</div>
      <div id="canvas2" style="display: none">canvas2</div>
      <div id="canvas3" style="display: none">canvas3</div>
      <div id="canvas4" style="display: none">
        <span @click.stop="deviceDetail(1)">设备详情</span>
      </div>
      <div id="canvas5" style="display: none">
        <span @click="deviceDetail(2)">设备详情</span>
      </div>
      <div id="canvas6" style="display: none">
        <span @click.stop="deviceDetail(3)">设备详情</span>
      </div>
      <div id="btns">
        <span @click.stop="toView(1)">主视角</span>
        <span @click.stop="toView(2)">俯视角</span>
      </div>
    </div>
  </div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
import TWEEN from '@tweenjs/tween.js'
import $ from 'jquery'

// import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
// import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import {
  CSS3DRenderer,
  CSS3DObject,
} from 'three/examples/jsm/renderers/CSS3DRenderer'
export default {
  name: '',
  data() {
    return {
      scene: null,
      camera: null,
      renderer: null,
      controls: null,
      cssScene: null,
      cssRender: null,
      cssControls: null,
      light: null,
      light2: null,
      flowingLineTexture: null,
      group: new THREE.Group(),
      composer: null, // 控制发光
      outlinePass: null,
      renderPass: null,
      // 选中的模型
      selectedObjects: [],
      mouse: new THREE.Vector2(),
      raycaster: new THREE.Raycaster(),
      tween: null,
      // 数据显示面板  1-3 数据    4-6为设备详情按钮
      dataPanel: {
        plane1: null,
        plane2: null,
        plane3: null,
        plane4: null,
        plane5: null,
        plane6: null,
      },
      // 管道纹理
      flowingLineTexture1: null,
      flowingLineTexture2: null,
    }
  },
  components: {},
  computed: {},
  watch: {},
  created() {},
  mounted() {
    if (this.scene !== null) {
      this.scene = null
    }
    this.draw()
  },
  destroyed() {
    this.scene.autoUpdate = false
    cancelAnimationFrame(this.animate)
    this.renderer.domElement.innerHTML = ''
    this.renderer.forceContextLoss()
    this.renderer.dispose()
    this.scene.children = []
  },
  methods: {
    initScene() {
      this.scene = new THREE.Scene()
      this.cssScene = new THREE.Scene()
    },
    initCamera() {
      this.camera = new THREE.PerspectiveCamera(
        45,
        window.innerWidth / window.innerHeight,
        0.1,
        50000
      )
      this.camera.position.set(0, 1000, 2900)
      // this.camera.lookAt(new THREE.Vector3(0,0,0))
    },
    initLight() {
      var ambientLight = new THREE.AmbientLight(0x404040)
      this.scene.add(ambientLight)

      this.light = new THREE.DirectionalLight(0x333333)
      this.light.position.set(20, 20, 20)
      this.light2 = new THREE.DirectionalLight(0xdddddd)
      this.light2.position.set(-20, 20, -20)
      this.scene.add(this.light)
      this.scene.add(this.light2)
    },
    initRender() {
      //dom元素渲染器
      this.cssRender = new CSS3DRenderer({ antialias: true })
      this.cssRender.setSize(window.innerWidth, window.innerHeight)
      this.cssRender.domElement.style.position = 'absolute'
      this.cssRender.domElement.style.top = '0'
      this.cssRender.domElement.style.outline = 'none'
      document.getElementById('model').appendChild(this.cssRender.domElement)

      this.renderer = new THREE.WebGLRenderer({ antialias: true })
      this.renderer.setSize(window.innerWidth, window.innerHeight)
      this.renderer.setClearColor(new THREE.Color(0x01050f))
      //window.devicePixelRatio 当前设备的物理分辨率与css分辨率之比
      this.renderer.setPixelRatio(window.devicePixelRatio)
      //按层级先后渲染
      this.renderer.sortObjects = true
      document.getElementById('model').appendChild(this.renderer.domElement)
    },
    initModel() {
      //坐标系
      var axes = new THREE.AxesHelper(4000)
      this.scene.add(axes)

      var planGeometry = new THREE.PlaneGeometry(3000, 2000)
      var planeMaterial = new THREE.MeshLambertMaterial({
        color: 0xcccccc,
        side: THREE.DoubleSide,
        // wireframe:true
      })
      var plane = new THREE.Mesh(planGeometry, planeMaterial)
      plane.rotation.x = -Math.PI / 2
      plane.position.set(0, 0, 0)
      this.scene.add(plane)

      var cubeGeometry = new THREE.BoxGeometry(400, 200, 200)
      var cubeMaterial = new THREE.MeshLambertMaterial({
        color: 0x00ff00,
        transparent: true,
        opacity: 0.8,
      })
      var cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
      cube.position.set(0, 120, 0)
      cube.name = 'cube1'
      this.scene.add(cube)

      var cubeGeometry2 = new THREE.BoxGeometry(400, 200, 200)
      var cubeMaterial2 = new THREE.MeshLambertMaterial({
        color: 0x00ffff,
        transparent: true,
        opacity: 0.8,
      })
      var cube2 = new THREE.Mesh(cubeGeometry2, cubeMaterial2)
      cube2.position.set(1000, 120, -500)
      cube2.name = 'cube2'
      this.scene.add(cube2)

      var cubeGeometry3 = new THREE.BoxGeometry(400, 200, 200)
      var cubeMaterial3 = new THREE.MeshLambertMaterial({
        color: 0xff0000,
        transparent: true,
        opacity: 0.8,
      })
      var cube3 = new THREE.Mesh(cubeGeometry3, cubeMaterial3)
      cube3.position.set(-1000, 100, 500)
      cube3.name = 'cube3'
      this.scene.add(cube3)
      // 生成管道
      this.generatePipe(1)
      this.generatePipe(2)

      // 生成数据显示面板
      this.generateDataPanel(1, 0, 400, 0)
      this.generateDataPanel(2, 1000, 400, -500)
      this.generateDataPanel(3, -1000, 400, 500)
      this.generateDataPanel(4, 100, 0, 120)
      this.generateDataPanel(5, 1000, 0, -400)
      this.generateDataPanel(6, -1000, 100, 700)

      // 出现数据显示面板
      for (var i = 1; i <= 3; i++) {
        $('#canvas' + i).css('display', 'block')
      }
      
      $('#btns').css('display', 'block')
    },
    //高亮显示模型(呼吸灯)
    outlineObj(selectedObjects) {
      // 创建一个EffectComposer(效果组合器)对象,然后在该对象上添加后期处理通道。
      this.composer = new EffectComposer(this.renderer)
      // 新建一个场景通道  为了覆盖到原理来的场景上
      this.renderPass = new RenderPass(this.scene, this.camera)
      this.composer.addPass(this.renderPass)
      // 物体边缘发光通道
      this.outlinePass = new OutlinePass(
        new THREE.Vector2(window.innerWidth, window.innerHeight),
        this.scene,
        this.camera,
        selectedObjects
      )

      this.outlinePass.edgeStrength = 5.0 // 高光边缘边框的亮度
      this.outlinePass.edgeGlow = 1 // 光晕[0,1]  边缘微光强度
      this.outlinePass.usePatternTexture = false // 是否使用父级的材质,纹理覆盖
      this.outlinePass.edgeThickness = 1 // 边框宽度,高光厚度
      this.outlinePass.downSampleRatio = 1 // 边框弯曲度
      this.outlinePass.pulsePeriod = 3 // 呼吸闪烁的速度,数值越大,律动越慢
      this.outlinePass.visibleEdgeColor.set(parseInt(0xff800)) // 呼吸显示的颜色
      this.outlinePass.hiddenEdgeColor = new THREE.Color(0, 0, 0) // 呼吸消失的颜色
      // this.outlinePass.clear = true
      this.composer.addPass(this.outlinePass) // 加入高光特效
      this.outlinePass.selectedObjects = selectedObjects // 需要高光的模型
    },
    // 鼠标点击模型
    onMouseClick(event) {
      //通过鼠标点击的位置计算出raycaster所需要的点的位置,以屏幕中心为原点,值的范围为-1到1
      this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
      this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
      // 通过鼠标点的位置和当前相机的矩阵计算出raycaster
      this.raycaster.setFromCamera(this.mouse, this.camera)
      // 获取raycaster直线和所有模型相交的数组集合
      var intersects = this.raycaster.intersectObjects(this.scene.children)
      if (intersects[0].object.geometry instanceof THREE.PlaneGeometry) {
        return
      } else {
        this.selectedObjects = []
        this.selectedObjects.push(intersects[0].object)
        this.outlineObj(this.selectedObjects)
        if (intersects[0].object.name === 'cube1') {
          this.initTween(0, 850, 1000)
          $('#canvas4').css('display', 'block')

        } else if (intersects[0].object.name === 'cube2') {
          this.initTween(1500, 500, -1000)
          $('#canvas5').css('display', 'block')
        } else if (intersects[0].object.name === 'cube3') {
          this.initTween(-1400, 400, 1000)
          $('#canvas6').css('display', 'block')
        }
      }
    },
    // 相机移动动画
    initTween(targetX, targetY, targetZ) {
      // 需要保留this
      var initPosition = {
        x: this.camera.position.x,
        y: this.camera.position.y,
        z: this.camera.position.z,
      }
      var tween = new TWEEN.Tween(initPosition)
        .to({ x: targetX, y: targetY, z: targetZ }, 2000)
        .easing(TWEEN.Easing.Sinusoidal.InOut)
      var onUpdate = (pos) => {
        var x = pos.x
        var y = pos.y
        var z = pos.z
        this.camera.position.set(x, y, z)
      }
      tween.onUpdate(onUpdate)
      tween.start()
      this.controls.target.set(0, 0, 0)
      this.cssControls.target.set(0, 0, 0)
    },
    // 生成数据显示面板
    generateDataPanel(id, x, y, z) {
      // CSS3DObject实现3D卡片
      var dataplane = document.getElementById('canvas' + id)
      this.dataPanel['plane' + id] = new CSS3DObject(dataplane)
      this.dataPanel['plane' + id].scale.set(1.2, 1.2, 1.2)
      this.dataPanel['plane' + id].position.set(x, y, z)
      this.cssScene.add(this.dataPanel['plane' + id])
    },
    // 生成管道
    generatePipe(id) {
      var curve
      var tubeGeometry
      var material
      if (id === 1) {
        curve = new THREE.CatmullRomCurve3([
          new THREE.Vector3(0, 200, 0),
          new THREE.Vector3(0, 200, -500),
          new THREE.Vector3(10, 200, -500),
          new THREE.Vector3(20, 200, -500),
          new THREE.Vector3(1000, 200, -500),
        ])
        tubeGeometry = new THREE.TubeGeometry(curve, 80, 10)
        this.flowingLineTexture1 = new THREE.TextureLoader().load(
          'static/arrow.png'
        )
        this.flowingLineTexture1.wrapS = this.flowingLineTexture1.wrapT =
          THREE.RepeatWrapping
        this.flowingLineTexture1.repeat.set(10, 1)
        this.flowingLineTexture1.needsUpdate = true
        material = new THREE.MeshBasicMaterial({
          map: this.flowingLineTexture1,
          side: THREE.DoubleSide,
          transparent: true,
        })
      } else if (id === 2) {
        curve = new THREE.CatmullRomCurve3([
          new THREE.Vector3(0, 200, 0),
          new THREE.Vector3(0, 200, 500),
          new THREE.Vector3(-10, 200, 500),
          new THREE.Vector3(-500, 200, 500),
          new THREE.Vector3(-800, 200, 500),
        ])
        tubeGeometry = new THREE.TubeGeometry(curve, 80, 10)
        this.flowingLineTexture2 = new THREE.TextureLoader().load(
          'static/arrow.png'
        )
        this.flowingLineTexture2.wrapS = this.flowingLineTexture2.wrapT =
          THREE.RepeatWrapping
        this.flowingLineTexture2.repeat.set(10, 1)
        this.flowingLineTexture2.needsUpdate = true
        material = new THREE.MeshBasicMaterial({
          map: this.flowingLineTexture2,
          side: THREE.DoubleSide,
          transparent: true,
        })
      }
      let tube = new THREE.Mesh(tubeGeometry, material)
      this.scene.add(tube)
    },
    // 视角改变
    toView(id) {
      if (id === 1) {
        this.initTween(0, 1000, 2900)
      } else if (id === 2) {
        this.initTween(0, 4000, 0)
      }
    },
    // 设备详情页面跳转
    deviceDetail(id) {
      console.log(id)
    },
    onWindowResize() {
      this.cssRender.setSize(window.innerWidth, window.innerHeight)
      this.renderer.setSize(window.innerWidth, window.innerHeight)
      this.camera.aspect = window.innerWidth / window.innerHeight
      this.camera.updateProjectionMatrix()
    },
    initControls() {
      this.cssControls = new OrbitControls(
        this.camera,
        this.cssRender.domElement
      )
      // //动态阻尼系数 即鼠标拖拽旋转的灵敏度
      // this.cssControls.dampingFactor = 0.25
      // this.cssControls.target.set(0, 900, 0)
      // //摄像机距离原点的距离
      // this.cssControls.minDistance = 1
      // this.cssControls.maxDistance = 20000
      // //上下旋转范围
      this.cssControls.minPolarAngle = 0
      this.cssControls.maxPolarAngle = 1.5
      // //左右旋转范围
      // this.cssControls.minAzimuthAngle = -Math.PI * 2
      // this.cssControls.maxAzimuthAngle = Math.PI * 2
      //是否开启右键拖拽
      this.cssControls.enabledPan = false

      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
      this.controls.dampingFactor = 0.25
      // // //缩放倍数
      this.controls.zoomSpeed = 1.0
      this.controls.minDistance = 1
      this.controls.maxDistance = 20000
      this.minPolarAngle = 0
      this.maxPolarAngle = 1.5
      this.minAzimuthAngle = -Math.PI * 2
      this.maxAzimuthAngle = Math.PI * 2
      this.controls.enabledPan = false
    },
    animate() {
      this.render()
      this.controls.update()
      this.cssControls.update()
      if (this.composer) {
        this.composer.render()
      }
      requestAnimationFrame(this.animate)
    },
    render() {
      // 数据显示面板的旋转角度与相机的旋转角度一致
      this.dataPanel.plane1.rotation.copy(this.camera.rotation)
      this.dataPanel.plane1.updateMatrix()
      this.dataPanel.plane2.rotation.copy(this.camera.rotation)
      this.dataPanel.plane2.updateMatrix()
      this.dataPanel.plane3.rotation.copy(this.camera.rotation)
      this.dataPanel.plane3.updateMatrix()
       this.dataPanel.plane4.rotation.copy(this.camera.rotation)
      this.dataPanel.plane4.updateMatrix()
       this.dataPanel.plane5.rotation.copy(this.camera.rotation)
      this.dataPanel.plane5.updateMatrix()
       this.dataPanel.plane6.rotation.copy(this.camera.rotation)
      this.dataPanel.plane6.updateMatrix()
      // 管道流动,更新管道纹理的偏移量
      // 管道流动速度
      var speed=0.01
      this.flowingLineTexture1.offset.x+=speed
      this.flowingLineTexture2.offset.x+=speed
      // 一定要激活
      TWEEN.update()
      this.renderer.render(this.scene, this.camera)
      this.cssRender.render(this.cssScene, this.camera)
    },
    draw() {
      this.initScene()
      this.initCamera()
      this.initLight()
      this.initRender()
      this.initModel()
      this.initControls()
      this.animate()
      window.onresize = this.onWindowResize
      window.onclick = this.onMouseClick
    },
  },
}
</script>

<style scoped lang="scss">
.about {
  width: 100%;
  height: 100%;

  #model {
    width: 100%;
    height: 100%;
    position: relative;
    overflow: hidden;
    #canvas1,
    #canvas2,
    #canvas3 {
      width: 150px;
      height: 200px;
      // background-color: rgba(8, 29, 54, 0.5);
      background-color: aquamarine;
      border-radius: 3px;
      // opacity: 0.5;
      color: red;
      font-size: 20px;
    }
    #canvas4,#canvas5,#canvas6{
      span{
        display: block;
        width: 100px;
        height: 40px;
        background-color: cornflowerblue;
        color: #fff;
        position: absolute;
        text-align: center;
        line-height: 40px;
        cursor: pointer;
      }
    }
    #btns {
      position: absolute;
      bottom: 0px;
      width: 100%;
      padding: 10px;
      text-align: center;
      z-index: 9999;
      span {
        width: 100px;
        display: inline-block;
        height: 30px;
        background-color: red;
        margin-right: 20px;
        cursor: pointer;
      }
    }
  }
}
</style>

你可能感兴趣的:(动画,three.js,vue.js)