vue+threejs控制相机:跟随物体

1,439 阅读4分钟

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


写在前面

本文用vue+threejs控制相机跟随物体。

有时候,我们想要展示场景中某个物体的视野,这时我们就需要用相机跟随物体。

以下是演示gif,用相机跟随移动的机器人,展示其移动的视野:

20221021_150008.gif

完整代码说明

  1. html和css

html中包含一个id容器用于插入three.js的渲染器dom节点,一个按钮,用来控制是否跟随

<template>
  <div class="item">
    <div id="THREE53"></div>
    <div class="btn_box">
      <el-button @click="followRobot">{{
        isFollow ? "取消跟随" : "跟随机器人"
      }}</el-button>
    </div>
  </div>
</template>

<style lang="less" scoped>
.btn_box {
  position: absolute;
  top: 10px;
  left: 210px;
}
</style>
  1. 引入threejs和需要的模块

OrbitControls轨道控制器,GLTFLoader模型加载器

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
  1. data()中的变量

camera相机,cameraPerspective跟随用的相机,scene场景,renderer场景渲染器,gltfLoader模型加载器,controls控制器,manager模型加载进度管理,clock时间,mixer混合动画器,Robot机器人模型,curve运动轨迹,isFollow是否跟随

  data() {
    return {
      camera: null,
      cameraPerspective: null,
      scene: null,
      renderer: null,
      gltfLoader: null,
      controls: null,
      manager: null,
      clock: null,
      mixer: null,
      Robot: null,
      curve: null,
      isFollow: false,
    };
  },
  1. mounted()中调用的方法

initScene创建场景,initCamera创建相机,initLight创建灯光,initRenderer创建渲染器,initControls创建轨道控制器,initGround创建地面,initCurve使用Catmull-Rom算法从一系列的点创建一条平滑的三维曲线,initModel加载模型,然后在模型加载完成后onLoad执行animate()方法

  mounted() {
    this.manager = new THREE.LoadingManager();
    this.gltfLoader = new GLTFLoader(this.manager);
    this.clock = new THREE.Clock();
    this.initScene();
    this.initCamera();
    this.initLight();
    this.initRenderer();
    this.initControls();
    this.initGround();
    this.initCurve();
    this.initModel();

    this.manager.onLoad = () => {
      this.animate();
      console.log("Loading complete!");
    };
  },
  1. 创建场景
    initScene() {
      this.scene = new THREE.Scene();
    },
  1. 创建相机

创建两个相机,一个是用来显示正常视野,一个用来显示跟随视野

    initCamera() {
      this.camera = new THREE.PerspectiveCamera(
        50,
        (window.innerWidth - 201) / window.innerHeight,
        1,
        1000
      ); // 透视相机
      this.camera.position.set(2, 2, 4); // 设置相机位置

      // 创建透视相机
      this.cameraPerspective = new THREE.PerspectiveCamera(
        75,
        (window.innerWidth - 201) / window.innerHeight,
        0.1,
        6
      );
    },
  1. 创建灯光

创建一个平行光和一个环境光

    initLight() {
      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)
      this.scene.add(light); // 将灯光添加到场景中

      const ambLight = new THREE.AmbientLight(0xf0f0f0, 0.1); // 环境光
      this.scene.add(ambLight);
    },
  1. 创建渲染器

创建渲染器并且将渲染器dom节点添加到html元素中

    initRenderer() {
      this.renderer = new THREE.WebGLRenderer({ antialias: true });
      this.renderer.outputEncoding = THREE.sRGBEncoding;
      this.renderer.setPixelRatio(window.devicePixelRatio);
      this.renderer.setSize(window.innerWidth - 201, window.innerHeight);
      document.getElementById("THREE53").appendChild(this.renderer.domElement);
    },
  1. 创建轨道控制器

加了这个可以通过鼠标控制界面的缩放平移旋转

    initControls() {
      this.controls = new OrbitControls(this.camera, this.renderer.domElement);
      this.controls.addEventListener("change", this.render);
    },
  1. 创建地面

创建一个灰色的地面

    initGround() {
      const ground = new THREE.Mesh(
        new THREE.BoxGeometry(4, 0.0015, 4),
        new THREE.MeshPhongMaterial({
          color: 0x999999,
          depthWrite: false,
          transparent: true,
          opacity: 1,
        })
      );
      ground.receiveShadow = true;
      ground.position.y = -0.0015;
      this.scene.add(ground);
    },
  1. 创建运动轨迹

使用Catmull-Rom算法,从一系列的点创建一条平滑的三维曲线,这里设置了四个点,分别是(1, 0, -1)、(1, 0, 1)、(-1, 0, 1)、(-1, 0, -1),并且设置曲线的类型为"centripetal",设置曲线闭合.closed = true

    initCurve() {
      this.curve = new THREE.CatmullRomCurve3([
        new THREE.Vector3(1, 0, -1),
        new THREE.Vector3(1, 0, 1),
        new THREE.Vector3(-1, 0, 1),
        new THREE.Vector3(-1, 0, -1),
      ]);
      this.curve.curveType = "centripetal";
      this.curve.closed = true;
      const points = this.curve.getPoints(50); // 获取点列表,50为要将曲线划分为的分段数
      const line = new THREE.LineLoop(
        new THREE.BufferGeometry().setFromPoints(points),
        new THREE.LineBasicMaterial({ color: 0x0000ff })
      ); // 一条头尾相接的连续的线(参数说明:顶点列表,材质)
      this.scene.add(line); // 将曲线添加到场景中
    },
  1. 加载模型(机器人)

使用gltfLoader加载器加载机器人模型,并激活机器人模型的动画

    initModel() {
      this.gltfLoader.load(
        "/models/models/gltf/RobotExpressive/RobotExpressive.glb",
        (gltf) => {
          gltf.scene.scale.set(0.1, 0.1, 0.1);
          this.Robot = gltf.scene;
          this.scene.add(gltf.scene);

          const animations = gltf.animations;
          this.mixer = new THREE.AnimationMixer(this.Robot); // 动画混合器
          let actions = {};
          for (let i = 0; i < animations.length; i++) {
            const clip = animations[i];
            const action = this.mixer.clipAction(clip);
            actions[clip.name] = action;
          }
          // 制定动画
          actions["Walking"]
            .reset() // 重置动作
            .play(); // 让混合器激活动作
        }
      );
    },
  1. 跟随代码和渲染场景

在animate()方法中更新机器人走路的动画,并且调整机器人走路的位置和朝向,通过曲线curve的.getPointAt(t)方法:参数t表示当前点在线条上的位置百分比,该方法根据传入的百分比返回在曲线上的位置;和.getTangentAt(t)方法:该方法根据传入的百分比返回在曲线上的位置的切线。

根据上面两个方法得到的结果更新机器人的位置和朝向,以及更新跟随相机的位置和朝向。

代码中的每一步都有详细说明。

最后render()方法渲染场景,如果开启了跟随,就用跟随相机,如果没开启跟随,就用平常的相机

    animate() {
      requestAnimationFrame(this.animate);

      // 执行动画
      const dt = this.clock.getDelta();
      if (this.mixer) {
        this.mixer.update(dt);
      }

      // 调整走路位置和朝向
      const loopTime = 25 * 1000; // 定义循环一圈所需的时间
      let time = Date.now();
      let t = (time % loopTime) / loopTime; // 计算当前时间进度百分比

      const position = this.curve.getPointAt(t);
      this.Robot.position.copy(position); // 更新机器人的位置

      const tangent = this.curve.getTangentAt(t);
      const lookAtVec = tangent.add(this.Robot.position); // 位置向量和切线向量相加即为所需朝向的点向量
      this.Robot.lookAt(lookAtVec); // 更新机器人朝向

      let time2 = time - 2 * 1000;
      let t2 = (time2 % loopTime) / loopTime; // 计算当前时间进度百分比
      const position2 = this.curve.getPointAt(t2); // 比上面模型在曲线上的的位置晚2s的位置
      this.cameraPerspective.position.set(
        position2.x,
        position2.y + 0.5,
        position2.z
      ); // 更新跟随用的相机的位置,这里让它向上偏移了0.5
      this.cameraPerspective.lookAt(position); // 跟随用的相机朝向机器人的位置

      this.render();
    },
    render() {
      if (this.isFollow) {
        this.renderer.render(this.scene, this.cameraPerspective);
      } else {
        this.renderer.render(this.scene, this.camera);
      }
    },
  1. 按钮事件

点击按钮切换是否跟随

    followRobot() {
      this.isFollow = !this.isFollow;
    },

写在最后

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