threejs+tweenjs реализует 3D-анимацию сцен

three.js
threejs+tweenjs реализует 3D-анимацию сцен

Эффект

随着前端越来越卷,对前端的要求也越来越高,从视觉效果上不断的迈向3d
一直想学习threejs,迫于没有时间,正好空闲下来一周(记录一下入门threejs)
✨实现一个3d换色车以及场景动画✨

Архитектура знаний

├── Tweenjs 动画插件 
└── Threejs 3d引擎
  └── Scence 场景
  └── WebGLRenderer 渲染器
  └── Camera 相机(这里用到的是透视相机PerspectiveCamera)
  └── Fog 雾化效果
  └── Lights 灯光效果
  └── OrbitControls 控制器
  └── animate 动画函数
  └── CubeTextureLoader 环境贴图
  └── Mesh 网格对象
      └── PlaneGeometry 创建矩形
      └── MeshLambertMaterial 材质
  └── FBXLoader fbx模型
  └── GLTFLoader gltf/glb模型
  └── Raycaster 模型拆分选中事件

Tweenjs

import TWEEN from '@tweenjs/tween.js'
setTweens(obj, newObj, duration = 1500) { 
  var ro = new TWEEN.Tween(obj) //创建tween动画实例
  ro.to(newObj, duration) //变化后的对象以及动画持续时间
  ro.easing(TWEEN.Easing.Sinusoidal.InOut) //动画缓动函数
  ro.onUpdate(function() {}) //执行回调
  ro.start()
},

Threejs

Документация Threejs

<template>
  <div>
    <div id="cart"></div>
    <div class="main">
      车身
      <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">
      内饰
      <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">
      上车
      <img style="width: 50px;height: 50px" src="/glb/into.png" alt="">
    </div>
    <div class="out" @click.stop="outCart">
      下车
      <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>
import * as THREE from '@/plugins/ys3d/threeLibs/three.module'
data() {
  return {
    loding: true, // 加载
    mainColor: '', // 颜色
    trimColor: '', // 颜色
    marker: '', // 车门标记
    selectedObjects: null, // 点击选中的模块
    renderer: null,
    main: null, // 车身
    trim: null, // 内饰
    cart: null, // 车模型
    wheel: null, // 车轮
    door: null, // 第一个车门
    scene: null,
    camera: null,
    mixer: null, // 车子模型动画
    mixer1: null, // 房子模型动画
    isOpen: false, // 是否打开车门
    controlsObj: null, // 操控画面对象
    plane: null, // 平面模型
    percentage: 0// 加载进度
  }
}
async init() {
      //创建场景
      this.scene = new THREE.Scene()
      //摄像头
      this.camera = new THREE.PerspectiveCamera(70, 1, 1, 1000)// 摄像头
      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')
      //添加雾化
      this.scene.fog = new THREE.Fog(0xffffff, 200, 1000)
      //创建光源
      this.lights(this.scene)
      //创建平面
      this.loadPlan(this.scene)
      //加载模型
      await this.loadFbx(this.scene, this.camera)
      await this.loadGlb(this.scene, this.camera)
      //创建环境贴图
      await this.initSky(this.scene)
      //创建控制器
      this.controls(this.scene, this.camera, this.renderer)
     this.renderer.render(this.scene, this.camera)
    }

Сцена сцены

const scene = new THREE.Scene()  场景可以理解为创建一个画布盒子

Рендерер WebGLRenderer

const renderer = new THREE.WebGLRenderer({
  antialias: true 是否抗锯齿
  ... 相关属性查看threejs文档
})

камера

camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000) 相机
camera.position.set(30, 10, 30) 相机位置
camera.lookAt(camera.position) 指相机看向三维中的某个位置

Распыление тумана

scene.fog = new THREE.Fog(0xffffff, 200, 1000) 远处添加边境线效果

Огни

new THREE.SpotLight 聚光灯
new THREE.PointLightHelper 灯光辅助容器
const sportLight = new THREE.SpotLight(0xffffff) 添加上方光源
sportLight.position.set(0, 1200, 0) 光源位置
scene.add(sportLight)
const sportLight1 = new THREE.SpotLight(0xffffff) 添加侧后方光源
sportLight1.position.set(-30, 20, 10)
scene.add(sportLight1)
const sportLight2 = new THREE.SpotLight(0xffffff) 添加后方光源
sportLight2.position.set(0, 0, 300)
scene.add(sportLight2)
const sportLight4 = new THREE.SpotLight(0xffffff) 添加侧前方光源
sportLight4.position.set(30, 0, 0)
scene.add(sportLight4)
// scene.add(new THREE.PointLightHelper(sportLight1, 1)) 添加灯光辅助容器(便于查看灯光位置)

Контроллер OrbitControls

new OrbitControls 创建控制器
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
// 动态阻尼系数 就是鼠标拖拽旋转灵敏度
// controls.dampingFactor = 0.5
// 是否可以缩放
controls.enableZoom = true
// 是否自动旋转
controls.autoRotate = true
controls.autoRotateSpeed = 0.3
// 设置相机距离原点的最远距离
controls.minDistance = 1
// 设置相机距离原点的最远距离
controls.maxDistance = 1200
// 是否开启右键拖拽
controls.enablePan = true

функция анимированной анимации

new THREE.Clock 时钟对象
clock.getDelta 当前的秒数
const that = this
const clock = new THREE.Clock() 时钟对象
function animate() {
  // controls.update() 控制器更新
  TWEEN.update() 动画更新
  // 平面移动(路面行驶效果)
  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)
  }
  // 车门标记点opacity动画
  if (that.marker.material.opacity >= 1) {
    that.marker.material.opacity = 0.1
  } else {
    that.marker.material.opacity += 0.01
  }
  // 车轮转动
  that.wheel.map(cell => {
    cell.rotation.set(cell.rotation.x + 1000, -0.5 * Math.PI, cell.rotation.z)
  })
  renderer.render(scene, camera) //重新渲染
  requestAnimationFrame(animate) //定时函数
  const delta = clock.getDelta() //当前的秒数
  if (that.mixer) {
    that.mixer.update(delta)
  }
  if (that.mixer1) {
    that.mixer1.update(delta)
  }
  if (that.mixer2) {
    that.mixer2.update(delta)
  }
}
animate()

Карта окружения CubeTextureLoader (скайбокс)

new THREE.CubeTextureLoader 创建天空盒子容器
initSky(scene) {
    return new Promise((resolve, reject) => {
      //加载正方体六个面的图片(p n正反面 xyz三维坐标轴)
      scene.background = new THREE.CubeTextureLoader().setPath('glb/').load(
        [
          'nx.jpg',
          'nz.jpg',
          'py.jpg',
          'DN.jpg',
          'px.jpg',
          'pz.jpg'
        ]
      )
      resolve()
    })
}

Сетчатый объект сетки

THREE.ImageUtils.loadTexture 加载图片
new THREE.Mesh 创建网格对象
new THREE.PlaneGeometry 创建矩形
new THREE.MeshLambertMaterial 创建材质
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(// 创建平面
      new THREE.PlaneGeometry(300, 2000),
      new THREE.MeshLambertMaterial({
        map: texture,
        side: THREE.DoubleSide,//双面材质
        opacity: 0.5, // 透明度
        transparent: true //是否透明
      })
    )
    const plane = new THREE.Mesh(// 创建水面
      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)
}

FBXLoader

Модель автомобиля

new FBXLoader fbx模型加载器
//小车模型
loadFbx(scene, camera) {
  const that = this
  const loader = new FBXLoader()
  return new Promise(resolve => {
    loader.load('/glb/MBSL.fbx', function(obj) {
      obj.name = 'cart'//对象名称
      that.cart = obj //小车对象
      obj.scale.set(0.004, 0.008, 0.004)//大小
      obj.position.set(0, 0, -4)//位置
      obj.rotation.set(0, -41, 0)
      that.wheel = obj.children[1].children[5].children//车轮模型组
      that.wheel.map(cell => {//设置车轮位置
        cell.rotation.set(cell.rotation.x, -0.5 * Math.PI, cell.rotation.z)
      })
      that.door = obj.children[1].children[3].children[0].children //车门模型组
      //车身模型
      that.main = [obj.children[1].children[0], ...obj.children[1].children[1].children, ...obj.children[1].children[3].children]
      //内饰模型
      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)//模型选中事件
    })
    resolve()
  })
}

дверной маркер

THREE.ImageUtils.loadTexture 图片加载器
new THREE.Mesh 网格对象
new THREE.PlaneGeometry 矩形对象
new THREE.MeshLambertMaterial 材质
new THREE.Group 组合对象
sign(obj){
 var texture = THREE.ImageUtils.loadTexture( //车门标记点
        '/glb/marker3.png',
        null, function(t) {
        }
      )
      that.marker = new THREE.Mesh(// 创建网格对象
        new THREE.PlaneGeometry(1, 1),//标记点
        new THREE.MeshLambertMaterial({
          map: texture,//标记点贴图
          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()//组合对象
      group.add(obj)
      group.add(marker)
      return group
}

GLTFLoader

new GLTFLoader 加载gltf/glb模型
new DRACOLoader 对模型进行压缩
new THREE.AnimationMixer 提取执行模型动画
loadGlb(scene, camera) {
  const loaderModule = new GLTFLoader()
  const that = this
  return new Promise(resolve => {
    const dracoLoader = new DRACOLoader()
    dracoLoader.setDecoderPath('glb/draco/') //设置DRACOLoader文件路径public
    loaderModule.setDRACOLoader(dracoLoader) 
    loaderModule.load('/glb/LittlestTokyo.glb', function(gltf) {
      //房子模型
      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) {
      //小鸟模型
      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)
    })
  })
}

Raycaster

new THREE.Raycaster 光线投射用于计算出鼠标经过哪些模型
//模型选中事件
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)
    // 这里我们只检测模型的选中情况
    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) { //关闭车门
            that.isOpen = false
            that.selectedObjects.parent.children.map(item => {
              // 动画
              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 => { //打开车门
              that.setTweens(item.rotation, {
                x: item.rotation.x,
                y: item.rotation.y - 0.5 * Math.PI,
                z: item.rotation.z
              }, 1000)
            })
          }
          break
      }
    }
  }, false)
}

функция смены цвета

model 模型对象
value 颜色值
递归实现修改材质
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)
    }
  })
}

в машину

通过延迟实现动画的连贯性
оригинальная перспектива
Открой дверь
Расширить перспективу
изменить положение автомобиля
вид водителя
закрой дверь
intoCart() {
  this.isOpen = true
  this.controlsObj.reset()// 回到初始化视角
  this.setTweens(this.camera.position, { x: 30, y: 10, z: 30 }, 1000)
  this.door.map(item => { //打开门
    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)//展开视角
    setTimeout(() => {
      this.setTweens(this.camera.position, { x: 0, y: 0, z: 1 }, 1000)//驾驶位视角
      this.setTweens(this.plane.position, { x: this.plane.position.x, y: -10, z: this.plane.position.z }, 500)//平面下移
      setTimeout(() => {
        this.cart.position.set(1.5, -6, 5)//车位置改变
        this.cart.rotation.set(0, 0, 0)
        this.camera.lookAt(this.camera.position)//查看视角
        this.isOpen = false
        this.door.map(item => {//关闭车门
          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)//重新渲染
      }, 1000)
    }, 1000)
  }, 1000)
}

выйти из машины

вид водителя
Открой дверь
оригинальная перспектива
закрой дверь
outCart() {
  this.isOpen = true
  this.door.map(item => {//打开车门
    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)//草坪回归原位
    this.setTweens(this.camera.position, { x: 30, y: 10, z: 30 }, 1000)//回到原视角
    this.cart.position.set(0, 0, -4)//小车回归原位
    this.cart.rotation.set(0, -41, 0)
    this.camera.lookAt(this.camera.position)
    this.controlsObj.reset()// 回到初始化视角
    setTimeout(() => {
      this.door.map(item => {
        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)
      })
    }, 1000)
  }, 1000)
}