Realization of 3D Scene animation based on Threejs + TweenJS (Hand-touch teaching)

The effect

With the front end is more and more volume, the requirements of the front end is also more and more high, from the visual effect of continuous towards 3D has always wanted to learn Threejs, forced to have no time, just idle down a week (record the entry threejs) ✨ to achieve a 3D color car and scene animation ✨Copy the code

Knowledge architecture

├─ ├─ webglesimally, ├─ ├─ webglesimally, └─ Fog ├ ─ uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press, uninhibited-press └ └─ MeshLambertMaterial ├ ─ FBXLoader ├ ─ GLTFLoader ├ ─ RaycasterCopy the code

Tweenjs

import TWEEN from '@tweenjs/tween.js'
setTweens(obj, newObj, duration = 1500) { 
  var ro = new TWEEN.Tween(obj) // Create a tween animation instance
  ro.to(newObj, duration) // The changed object and the animation duration
  ro.easing(TWEEN.Easing.Sinusoidal.InOut) // Animation buffer function
  ro.onUpdate(function() {}) // Perform the callback
  ro.start()
},
Copy the code

Threejs

Threejs document

<template>
  <div>
    <div id="cart"></div>
    <div class="main">body<div class="flex">
        <img style="width: 100px; height: 50px" src="/glb/cart.png" alt="">
        <el-color-picker v-model="mainColor"></el-color-picker>
      </div>
    </div>
    <div class="tirm">interior<div class="flex">
        <img style="width: 100px; height: 50px" src="/glb/trim.png" alt="">
        <el-color-picker v-model="trimColor"></el-color-picker>
      </div>
    </div>
    <div class="into" @click="intoCart">Get on the bus<img style="width: 50px; height: 50px" src="/glb/into.png" alt="">
    </div>
    <div class="out" @click.stop="outCart">Get off the bus<img style="width: 50px; height: 50px" src="/glb/out.png" alt="">
    </div>
    <div v-show="loding" v-loading.fullscreen.lock="loding" class="mask"></div>
    <div
      v-show="loding"
      style="z-index: 9999; position: absolute; top: 53%; left: 50%; width: 300px; transform:translate(-50%,0)"
    >
      <el-progress :text-inside="true" :stroke-width="26" :percentage="this.percentage"></el-progress>
    </div>
  </div>
</template>
Copy the code
import * as THREE from '@/plugins/ys3d/threeLibs/three.module'
data() {
  return {
    loding: true./ / load
    mainColor: ' './ / color
    trimColor: ' './ / color
    marker: ' '.// Open the door
    selectedObjects: null.// Click the selected module
    renderer: null.main: null./ / body
    trim: null./ / interior
    cart: null./ / car model
    wheel: null./ / wheel
    door: null.// The first door
    scene: null.camera: null.mixer: null.// Car model animation
    mixer1: null.// House model animation
    isOpen: false.// Whether to open the door
    controlsObj: null.// Manipulate the screen object
    plane: null.// Plane model
    percentage: 0// Load progress}}async init() {
      // Create a scene
      this.scene = new THREE.Scene()
      / / camera
      this.camera = new THREE.PerspectiveCamera(70.1.1.1000)/ / camera
      this.renderer = new THREE.WebGLRenderer({
      antialias: true
      })
      this.renderer.setSize(window.innerWidth, window.innerHeight)
      document.getElementById('cart').append(this.renderer.domElement)
      this.renderer.setClearColor('# 000000')
      // Add atomizer
      this.scene.fog = new THREE.Fog(0xffffff.200.1000)
      // Create light source
      this.lights(this.scene)
      // Create plane
      this.loadPlan(this.scene)
      // Load the model
      await this.loadFbx(this.scene, this.camera)
      await this.loadGlb(this.scene, this.camera)
      // Create an environment map
      await this.initSky(this.scene)
      // Create a controller
      this.controls(this.scene, this.camera, this.renderer)
     this.renderer.render(this.scene, this.camera)
    }
Copy the code

Scene Scene

const scene = newTHREE.Scene() can be thought of as creating a canvas boxCopy the code

WebGLRenderer renderer

const renderer = new THREE.WebGLRenderer({
  antialias: trueAnti-aliasing... Related properties view threejs documentation})Copy the code

Camera Camera

camera = new THREE.PerspectiveCamera(70.window.innerWidth / window.innerHeight, 1.1000) camera. The camera position. Set (30.10.30Camera position camera.lookat (camera.position) means that the camera looks at a position in three dimensionsCopy the code

Fog spray

scene.fog = new THREE.Fog(0xffffff.200.1000) added border effect in the distanceCopy the code

Towns light source

New THREE.SpotLight New THREE.PointLightHelper Light auxiliary containerCopy the code
const sportLight = new THREE.SpotLight(0xffffffSportlight.position.set (sportlight.position.set)0.1200.0) Light source location scene.add(sportLight)const sportLight1 = new THREE.SpotLight(0xffffff) add sportlight1.position.set (-)30.20.10)
scene.add(sportLight1)
const sportLight2 = new THREE.SpotLight(0xffffffSportlight2.position.set ()0.0.300)
scene.add(sportLight2)
const sportLight4 = new THREE.SpotLight(0xffffffSportlight4.position.set ()30.0.0)
scene.add(sportLight4)
// scene.add(new three.pointlighthelper (sportLight1, 1))
Copy the code

OrbitControls controller

New OrbitControls creates the controllerCopy the code
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
// The dynamic damping coefficient is the mouse drag rotation sensitivity
/ / controls. DampingFactor = 0.5
// Whether it can be scaled
controls.enableZoom = true
// Whether to rotate automatically
controls.autoRotate = true
controls.autoRotateSpeed = 0.3
// Set the maximum distance of the camera from the origin
controls.minDistance = 1
// Set the maximum distance of the camera from the origin
controls.maxDistance = 1200
// Whether to enable right-click drag
controls.enablePan = true
Copy the code

Animate function

New THREE.Clock Clock object clock.getDelta Current number of secondsCopy the code
const that = this
const clock = newTHREE.Clock() Clock objectfunction animate() {
  // controls.update() Updates the controllerTween.update () animation update// Plane movement (road driving effect)
  if (that.plane.position.z < -600) { 
    that.plane.position.set(0, that.plane.position.y, 0)}else {
    that.plane.position.set(that.plane.position.x, that.plane.position.y, that.plane.position.z - 2)}// Animates the car door
  if (that.marker.material.opacity >= 1) {
    that.marker.material.opacity = 0.1
  } else {
    that.marker.material.opacity += 0.01
  }
  // The wheel turns
  that.wheel.map(cell= > {
    cell.rotation.set(cell.rotation.x + 1000, -0.5 * Math.PI, cell.rotation.z)
  })
  renderer.render(scene, camera) // re-render
  requestAnimationFrame(animate) // the timer function
  const delta = clock.getDelta() // Current number of seconds
  if (that.mixer) {
    that.mixer.update(delta)
  }
  if (that.mixer1) {
    that.mixer1.update(delta)
  }
  if (that.mixer2) {
    that.mixer2.update(delta)
  }
}
animate()
Copy the code

CubeTextureLoader Environment map (Sky Box)

New three. CubeTextureLoader creates the skybox containerCopy the code
initSky(scene) {
    return new Promise((resolve, reject) = > {
      // load the image of the six faces of the cube (p n front and back xyz 3d axes)
      scene.background = new THREE.CubeTextureLoader().setPath('glb/').load(
        [
          'nx.jpg'.'nz.jpg'.'py.jpg'.'DN.jpg'.'px.jpg'.'pz.jpg'
        ]
      )
      resolve()
    })
}
Copy the code

Mesh object

THREE. ImageUtils. LoadTexture loading picture new THREE. The Mesh to create new grid object. THREE PlaneGeometry create rectangle new THREE. MeshLambertMaterial texturingCopy the code
loadPlan(scene) {
    var texture = THREE.ImageUtils.loadTexture(
      '/glb/grasslight-big.jpg'.null.function(t) {})var texture1 = THREE.ImageUtils.loadTexture(
      '/glb/water.jpg'.null.function(t) {})this.plane = new THREE.Mesh(// Create plane
      new THREE.PlaneGeometry(300.2000),
      new THREE.MeshLambertMaterial({
        map: texture,
        side: THREE.DoubleSide,// Double-sided material
        opacity: 0.5./ / transparency
        transparent: true // Whether transparent}))const plane = new THREE.Mesh(// Create water
      new THREE.PlaneGeometry(2500.2500),
      new THREE.MeshLambertMaterial({
        map: texture1,
        side: THREE.DoubleSide
      })
    )
    this.plane.rotation.x = -0.5 * Math.PI
    this.plane.rotation.z = -1 * Math.PI
    this.plane.position.set(0, -2.5.0)
    plane.rotation.x = -0.5 * Math.PI
    plane.rotation.z = -1 * Math.PI
    plane.position.set(0, -14.0)
    scene.add(this.plane)
    scene.add(plane)
}
Copy the code

FBXLoader

Car model

New FBXLoader FBX model loaderCopy the code
// Car model
loadFbx(scene, camera) {
  const that = this
  const loader = new FBXLoader()
  return new Promise(resolve= > {
    loader.load('/glb/MBSL.fbx'.function(obj) {
      obj.name = 'cart'// Object name
      that.cart = obj // Car object
      obj.scale.set(0.004.0.008.0.004)/ / size
      obj.position.set(0.0, -4)/ / position
      obj.rotation.set(0, -41.0)
      that.wheel = obj.children[1].children[5].children// Wheel model group
      that.wheel.map(cell= > {// Set the wheel position
        cell.rotation.set(cell.rotation.x, -0.5 * Math.PI, cell.rotation.z)
      })
      that.door = obj.children[1].children[3].children[0].children // The door model group
      // Body model
      that.main = [obj.children[1].children[0], ...obj.children[1].children[1].children, ... obj.children[1].children[3].children]
      // Interior model
      that.trim = [...obj.children[1].children[2].children, ... obj.children[1].children[4].children, ... obj.children[1].children[6].children]
      scene.add(this.sign())
      that.domClick(camera, obj, that)// Model selected events
    })
    resolve()
  })
}
Copy the code

Door marker

THREE. ImageUtils. LoadTexture picture loader new THREE. The new Mesh grid object. THREE new PlaneGeometry rectangular object. THREE MeshLambertMaterial new material Group Group objectCopy the code
sign(obj){
 var texture = THREE.ImageUtils.loadTexture( // Door marker
        '/glb/marker3.png'.null.function(t) {
        }
      )
      that.marker = new THREE.Mesh(// Create a grid object
        new THREE.PlaneGeometry(1.1),/ / marked points
        new THREE.MeshLambertMaterial({
          map: texture,// Mark the dot map
          aoMapIntensity: 0.side: THREE.DoubleSide,
          opacity: 0.1.transparent: true.depthTest: false
        })
      )
      that.marker.position.set(2.5.3.3)
      that.marker.rotation.y = 0.5 * Math.PI
      var group = new THREE.Group()// Combine objects
      group.add(obj)
      group.add(marker)
      return group
}
Copy the code

GLTFLoader

New DRACOLoader compacts the GLTF/GLB model. New THREE.AnimationMixer extracts and executes the model animationCopy the code
loadGlb(scene, camera) {
  const loaderModule = new GLTFLoader()
  const that = this
  return new Promise(resolve= > {
    const dracoLoader = new DRACOLoader()
    dracoLoader.setDecoderPath('glb/draco/') // Set DRACOLoader file path public
    loaderModule.setDRACOLoader(dracoLoader) 
    loaderModule.load('/glb/LittlestTokyo.glb'.function(gltf) {
      // House model
      const object = gltf.scene
      object.position.set(-300.62, -500)
      object.rotation.set(0.0.5 * Math.PI, 0)
      object.traverse(function(node) {
        if (node.isMesh) node.castShadow = true
      })
      // get the animation
      that.mixer1 = new THREE.AnimationMixer(object)
      that.mixer1.clipAction(gltf.animations[0]).play()
      object.scale.set(0.3.0.3.0.3)
      // add object to scene
      scene.add(object)
      resolve()
    }, this.onProgress)
    loaderModule.load('/glb/Parrot.glb'.function(gltf) {
      // Bird model
      const object = gltf.scene
      object.position.set(-150.60, -100)
      object.traverse(function(node) {
        if (node.isMesh) node.castShadow = true
      })
      // get the animation
      that.mixer2 = new THREE.AnimationMixer(object)
      that.mixer2.clipAction(gltf.animations[0]).play()
      object.scale.set(0.1.0.1.0.1)
      // add object to scene
      scene.add(object)
    })
  })
}
Copy the code

Raycaster

New THREE.Raycaster is used to calculate which models the mouse passes overCopy the code
// Model selected events
domClick(camera, fbx, that) {
  const raycaster = new THREE.Raycaster()
  const mouse = new THREE.Vector2()
  document.addEventListener('click'.function(ev) {
    mouse.x = (ev.clientX / window.innerWidth) * 2 - 1
    mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1
    raycaster.setFromCamera(mouse, camera)
    // Here we only check the selected model
    const intersects = raycaster.intersectObjects(fbx.children, true)

    if (intersects.length > 0) {
      that.selectedObjects = intersects[0].object
      console.log('selectedObjects:', that.selectedObjects)
      switch (that.selectedObjects.name) {
        case 'DLP_lak':
          if (that.isOpen) { // Close the door
            that.isOpen = false
            that.selectedObjects.parent.children.map(item= > {
              / / animation
              that.setTweens(item.rotation, {
                x: item.rotation.x,
                y: item.rotation.y + 0.5 * Math.PI,
                z: item.rotation.z
              }, 1000)})}else {
            that.isOpen = true
            that.selectedObjects.parent.children.map(item= > { // Open the door
              that.setTweens(item.rotation, {
                x: item.rotation.x,
                y: item.rotation.y - 0.5 * Math.PI,
                z: item.rotation.z
              }, 1000)})}break}}},false)}Copy the code

Color changing function

Model Model object value Color value recursive implementation to modify the materialCopy the code
setColor(model, value) {
  model.map(cell= > {
    if (cell.children.length === 0) {
      const material = cell.material.clone()
      material.color = new THREE.Color(value)
      cell.material = material
    } else {
      this.setColor(cell.children, value)
    }
  })
}
Copy the code

Into the car

Animation coherence is achieved through delayCopy the code
Graph LR Original View --> Open doors --> Expand view --> Change car position --> Driver view --> Close doors
intoCart() {
  this.isOpen = true
  this.controlsObj.reset()// Go back to the initialization perspective
  this.setTweens(this.camera.position, { x: 30.y: 10.z: 30 }, 1000)
  this.door.map(item= > { / / open the door
    this.setTweens(item.rotation, {
      x: item.rotation.x,
      y: item.rotation.y - 0.5 * Math.PI,
      z: item.rotation.z
    }, 1000)})setTimeout(() = > {
    this.setTweens(this.camera.position, { x: 35.y: 15.z: 30 }, 1000)// Expand the perspective
    setTimeout(() = > {
      this.setTweens(this.camera.position, { x: 0.y: 0.z: 1 }, 1000)// Driving Angle
      this.setTweens(this.plane.position, { x: this.plane.position.x, y: -10.z: this.plane.position.z }, 500)// Move the plane down
      setTimeout(() = > {
        this.cart.position.set(1.5, -6.5)// The car position changes
        this.cart.rotation.set(0.0.0)
        this.camera.lookAt(this.camera.position)// View the view
        this.isOpen = false
        this.door.map(item= > {// Close the door
          this.setTweens(item.rotation, {
            x: item.rotation.x,
            y: item.rotation.y + 0.5 * Math.PI,
            z: item.rotation.z
          }, 1000)})this.renderer.render(this.scene, this.camera)// re-render
      }, 1000)},1000)},1000)}Copy the code

Out of the car

Graph LR Driver's view --> Open door --> Original view --> Close door
outCart() {
  this.isOpen = true
  this.door.map(item= > {// Open the door
    this.setTweens(item.rotation, {
      x: item.rotation.x,
      y: item.rotation.y - 0.5 * Math.PI,
      z: item.rotation.z
    }, 1000)})setTimeout(() = > {
    this.setTweens(this.plane.position, { x: this.plane.position.x, y: -2.5.z: this.plane.position.z }, 500)// Return the lawn to its original position
    this.setTweens(this.camera.position, { x: 30.y: 10.z: 30 }, 1000)// Return to the original perspective
    this.cart.position.set(0.0, -4)// Return the car to its original position
    this.cart.rotation.set(0, -41.0)
    this.camera.lookAt(this.camera.position)
    this.controlsObj.reset()// Go back to the initialization perspective
    setTimeout(() = > {
      this.door.map(item= > {
        this.setTweens(item.rotation, {// Close the door
          x: item.rotation.x,
          y: item.rotation.y + 0.5 * Math.PI,
          z: item.rotation.z
        }, 1000)
        this.renderer.render(this.scene, this.camera)
      })
    }, 1000)},1000)}Copy the code