vue+threejs写物体动画:移动到地面某位置

550 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情


写在前面

本文用vue+threejs写物体动画:移动到某位置。

实际中,我们需要把一个物体从一个位置移动到另一个位置,本文简单实现了这个功能。

下面是演示gif:

20220929_161120.gif

完整代码说明

  1. 一个id容器,用于添加threejs渲染器
<template>
  <div class="item">
    <div id="THREE42"></div>
  </div>
</template>
  1. 引入需要的threejs模块
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
  1. mounted中调用initThreejs
mounted() {
    this.initThreejs();
},
  1. initThreejs思路说明,完整代码中有更详细的解释

(1)创建场景 scene = new THREE.Scene();并且设置背景颜色scene.background = new THREE.Color(0x000000);

(2)创建灯光 const light = new THREE.DirectionalLight(0xffffff);创建一个平行光并设置平行光的光照方向。

(3)创建相机 PerspectiveCamera:透视相机,通过camera.position设置相机的位置,会影响看物体的角度和距离。

(4)创建渲染器 WebGLRenderer,并将渲染器添加到id容器中document.getElementById("THREE42").appendChild(renderer.domElement);

(5)创建光线投射 光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体),返回值就是鼠标所在位置的物体的集合(只对objects中的物体响应)

(6)创建地面 立方体BoxGeometry+灰色材质MeshPhongMaterial组成一个灰色的地面

(7)创建立方体 立方体BoxGeometry+红色材质MeshPhongMaterial组成一个红色的物体

(8)监听鼠标、手指落下事件 document.addEventListener("pointerdown", onPointerDown);

(9)onPointerDown方法中算出要移动的距离和要移动的方向的单位向量。

(10)animate方法中算出每次帧动画要移动的距离chaDis和已经移动的距离alreadyDis,通过translateOnAxis方法进行物体的移动。

每次移动没有固定距离,而是固定一个总时间,无论移动多远的距离,所花费的时间都是一样的,也可以改一下代码,改成相同时间移动的距离一样,可以用clock.getDelta(),执行一次.getDelta ()方法,再执行一次.getDelta ()方法,第二次执行.getDelta ()方法的时候,可以返回上次调用该方法到本次调用之间的时间间隔,返回间隔时间单位是秒。

  1. initThreejs完整代码
initThreejs() {
  let camera, scene, renderer;

  let clock = new THREE.Clock();

  let redMesh; // 要移动的对象

  let raycaster, // 光线投射,用于鼠标拾取,返回鼠标在的位置的物体
    pointer, // 二维向量,用于存放鼠标位置
    objects = [], // 用于存放响应鼠标拾取的对象
    distance, // 要移动的距离
    unitVector; // 移动方向的单位向量

  let chaDis, alreadyDis;

  init();

  function init() {
    // 创建场景
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000000); // 设置场景背景颜色

    // 创建灯光
    const light = new THREE.DirectionalLight(0xffffff); // 平行光
    light.position.set(0.5, 1.0, 0.5).normalize(); // 设置平行光的方向,从(0.5, 1.0, 0.5)->target一般(0, 0, 0)
    scene.add(light); // 将灯光添加到场景中

    // 创建相机
    camera = new THREE.PerspectiveCamera(
      35,
      (window.innerWidth - 201) / window.innerHeight,
      1,
      500
    ); // 透视相机
    camera.position.x = 36;
    camera.position.y = 67; // 设置相机的位置
    camera.position.z = 67;
    scene.add(camera); // 将相机添加到场景中

    // 创建渲染器
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.outputEncoding = THREE.sRGBEncoding;
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth - 201, window.innerHeight);
    document.getElementById("THREE42").appendChild(renderer.domElement);

    document.addEventListener("pointerdown", onPointerDown); // 监听鼠标、手指落下

    // 光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体),返回值就是鼠标在的物体的集合
    raycaster = new THREE.Raycaster();
    pointer = new THREE.Vector2(); // 定义一个二维向量

    // 创建轨道控制器
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.addEventListener("change", render);
    controls.update();

    // 创建地面
    const ground = new THREE.Mesh(
      new THREE.BoxGeometry(50, 0.15, 50),
      new THREE.MeshPhongMaterial({
        color: 0x999999,
        depthWrite: false,
        transparent: true,
        opacity: 1,
      })
    );
    ground.receiveShadow = true;
    scene.add(ground);

    objects.push(ground);

    // 创建立方体
    redMesh = new THREE.Mesh(
      new THREE.BoxGeometry(2, 2, 2),
      new THREE.MeshBasicMaterial({ color: 0xff0000 })
    ); // 网格模型
    redMesh.position.y = 1;
    scene.add(redMesh); // 将这个立方体添加到场景中

    render();
  }

  function onPointerDown(event) {
    pointer.set(
      (event.offsetX / (window.innerWidth - 201)) * 2 - 1,
      -(event.offsetY / window.innerHeight) * 2 + 1
    ); // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)

    raycaster.setFromCamera(pointer, camera); // 通过相机和鼠标位置更新射线

    const intersects = raycaster.intersectObjects(objects, false); // 获取光线投射在地面上活物体上的对象
    if (intersects.length > 0) {
      const intersect = intersects[0];
      // redMesh.position.copy(intersect.point).add(intersect.face.normal); // 将物体的位置移到鼠标点击的位置

      let subVector = new THREE.Vector3(); // 存放向量intersect.point - redMesh.position
      subVector.subVectors(intersect.point, redMesh.position); // 返回向量intersect.point - redMesh.position
      distance = redMesh.position.distanceTo(intersect.point); // 返回向量intersect.point 到 redMesh.position 的距离

      unitVector = subVector.add(intersect.face.normal).normalize(); // 对subVector的z方向位置调整,并且将调整的向量变成方向向量
      
      clock.start(); // 开始计时
      chaDis = 0;
      alreadyDis = 0;
      animate();
    }
  }

  function animate() {
    requestAnimationFrame(animate);
    let elapsedTime = clock.getElapsedTime(); // 获取计时开始到现在的总时长
    let allTime = 2;
    if (elapsedTime / allTime <= 1) {
      chaDis = (elapsedTime / allTime) * distance - alreadyDis; // 距离线性变化,匀速移动
      alreadyDis += chaDis;
      redMesh.translateOnAxis(unitVector, chaDis); // .translateOnAxis(平移方向的单位向量,将要平移的距离)
    }

    render();
  }

  function render() {
    renderer.render(scene, camera);
  }
},

写在最后

以上就是所有的代码和说明。