头图

场景

作为刚刚接触 Three.js 的小白,在工作中遇到下面的需求:

  • 加载一个 3D 模型
  • 通过代码切换预设的任意模型的视角

最终效果(在线示例):

👆基于官方示例增加的控制代码

在这里插入图片描述

我们通过官方示例可以知道,只要使用 OrbitControls 就可以通过鼠标调整模型的视角。可是,能不能通过代码,切换特定的视角呢?有没有官方的 API 可以实现这个交互呢?小白暂时未能找到拿来即用的示例代码。

基本原理

通过 AI 可以得知相关矩阵变化的公式原理:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

通过 AI,我们还能得到上面原理的核心算法代码:

  // Function to multiply matrix and point
  function multiplyMatrixAndPoint(matrix, point) {
    let result = []
    for (let i = 0; i < matrix.length; i++) {
      result[i] =
        matrix[i][0] * point[0] +
        matrix[i][1] * point[1] +
        matrix[i][2] * point[2]
    }
    return result
  }

  function rotatePoint(x, y, z, rotateZ, rotateY) {
    // Convert degrees to radians
    const radZ = (rotateZ * Math.PI) / 180
    const radY = (rotateY * Math.PI) / 180

    // Rotation matrix around Z-axis
    const Rz = [
      [Math.cos(radZ), -Math.sin(radZ), 0],
      [Math.sin(radZ), Math.cos(radZ), 0],
      [0, 0, 1],
    ]

    // Rotation matrix around Y-axis
    const Ry = [
      [Math.cos(radY), 0, Math.sin(radY)],
      [0, 1, 0],
      [-Math.sin(radY), 0, Math.cos(radY)],
    ]

    // Apply rotation matrices
    let pointAfterZ = this.multiplyMatrixAndPoint(Rz, [x, y, z])
    let finalPoint = this.multiplyMatrixAndPoint(Ry, pointAfterZ)

    return finalPoint
  }

输入:起始点 x, y, z;绕着 Z 轴旋转的角度;绕着 Y 轴旋转的角度;

输出:旋转后的新坐标 [x,y,z];

动画

基于上面坐标的计算,还要实现新旧坐标变化的过渡动画,这里可以用浏览器 API 的 requestAnimationFrame 实现:

  /**
   * 淡入淡出
   * https://www.cnblogs.com/cloudgamer/archive/2009/01/06/Tween.html
   * @param t 当前时间
   * @param b 初始值
   * @param c 增/减量
   * @param d 持续时间
   */
  cubicEaseInOut(t, b, c, d) {
    if ((t /= d / 2) < 1) return (c / 2) * t * t * t + b
    return (c / 2) * ((t -= 2) * t * t + 2) + b
  }

  rotateAnimate(fromZ, toZ, fromY, toY, fromDistance, toDistance, duration) {
    return new Promise((resolve) => {
      let time = 0

      cancelAnimationFrame(this.timer)
      const fn = () => {
        let degZ = this.cubicEaseInOut(time, fromZ, toZ - fromZ, duration)
        let degY = this.cubicEaseInOut(time, fromY, toY - fromY, duration)
        let distance =
          this.cubicEaseInOut(
            time,
            fromDistance * 1000,
            toDistance * 1000 - fromDistance * 1000,
            duration,
          ) / 1000

        if (time < duration) {
          time += 10
        }

        if (time >= duration) {
          degZ = toZ
          degY = toY
        }

        if (camera) {
          // 旋转后
          const posRotated = this.rotatePoint(
            this.cameraPos.x,
            this.cameraPos.y,
            this.cameraPos.z,
            degZ,
            degY,
          )

          // 缩放后
          const posZoomed = this.zoom(
            posRotated[0],
            posRotated[1],
            posRotated[2],
            distance,
          )

          // 偏移后
          this.camera.position.x = posZoomed[0]
          this.camera.position.y = posZoomed[1]
          this.camera.position.z = posZoomed[2]

          camera.lookAt(scene.position)
        }

        if (time < duration) {
          this.timer = requestAnimationFrame(fn)
        } else {
          cancelAnimationFrame(this.timer)

          resolve(true)
        }
      }

      this.timer = requestAnimationFrame(fn)
    })
  }

以 Z 轴旋转为例,主要逻辑就是 fromZ, toZ,代表从原来的角度 fromZ 渐变成新的角度 toZ,通过缓动函数 cubicEaseInOut,得出每一帧动画需要变化的角度,该角度通过上面的矩阵变换旋转,就可以得出该帧的新坐标。

把这些逻辑包装一下:

// 切换镜头视角工具
class ViewAnimate {
  scene
  camera

  timer = 0

  lastRotateZ = 0
  lastRotateY = 0
  lastDistance = 1

  cameraPos = {
    x: 0,
    y: 0,
    z: 0,
  }

  radius = 0

  constructor(scene, camera, cameraPos) {
    this.scene = scene
    this.camera = camera
    this.cameraPos = cameraPos
    this.radius = Math.abs(cameraPos.x)
  }

  /**
   * 淡入淡出
   * https://www.cnblogs.com/cloudgamer/archive/2009/01/06/Tween.html
   * @param t 当前时间
   * @param b 初始值
   * @param c 增/减量
   * @param d 持续时间
   */
  cubicEaseInOut(t, b, c, d) {
    if ((t /= d / 2) < 1) return (c / 2) * t * t * t + b
    return (c / 2) * ((t -= 2) * t * t + 2) + b
  }

  // Function to multiply matrix and point
  multiplyMatrixAndPoint(matrix, point) {
    let result = []
    for (let i = 0; i < matrix.length; i++) {
      result[i] =
        matrix[i][0] * point[0] +
        matrix[i][1] * point[1] +
        matrix[i][2] * point[2]
    }
    return result
  }

  rotatePoint(x, y, z, rotateZ, rotateY) {
    // Convert degrees to radians
    const radZ = (rotateZ * Math.PI) / 180
    const radY = (rotateY * Math.PI) / 180

    // Rotation matrix around Z-axis
    const Rz = [
      [Math.cos(radZ), -Math.sin(radZ), 0],
      [Math.sin(radZ), Math.cos(radZ), 0],
      [0, 0, 1],
    ]

    // Rotation matrix around Y-axis
    const Ry = [
      [Math.cos(radY), 0, Math.sin(radY)],
      [0, 1, 0],
      [-Math.sin(radY), 0, Math.cos(radY)],
    ]

    // Apply rotation matrices
    let pointAfterZ = this.multiplyMatrixAndPoint(Rz, [x, y, z])
    let finalPoint = this.multiplyMatrixAndPoint(Ry, pointAfterZ)

    return finalPoint
  }

  zoom(x, y, z, distance) {
    const m = [
      [distance, 0, 0],
      [0, distance, 0],
      [0, 0, distance],
    ]
    const pointAfterZ = this.multiplyMatrixAndPoint(m, [x, y, z])
    return pointAfterZ
  }

  rotateAnimate(fromZ, toZ, fromY, toY, fromDistance, toDistance, duration) {
    return new Promise((resolve) => {
      let time = 0

      cancelAnimationFrame(this.timer)
      const fn = () => {
        let degZ = this.cubicEaseInOut(time, fromZ, toZ - fromZ, duration)
        let degY = this.cubicEaseInOut(time, fromY, toY - fromY, duration)
        let distance =
          this.cubicEaseInOut(
            time,
            fromDistance * 1000,
            toDistance * 1000 - fromDistance * 1000,
            duration,
          ) / 1000

        if (time < duration) {
          time += 10
        }

        if (time >= duration) {
          degZ = toZ
          degY = toY
        }

        if (camera) {
          // 旋转后
          const posRotated = this.rotatePoint(
            this.cameraPos.x,
            this.cameraPos.y,
            this.cameraPos.z,
            degZ,
            degY,
          )

          // 缩放后
          const posZoomed = this.zoom(
            posRotated[0],
            posRotated[1],
            posRotated[2],
            distance,
          )

          // 偏移后
          this.camera.position.x = posZoomed[0]
          this.camera.position.y = posZoomed[1]
          this.camera.position.z = posZoomed[2]

          camera.lookAt(scene.position)
        }

        if (time < duration) {
          this.timer = requestAnimationFrame(fn)
        } else {
          cancelAnimationFrame(this.timer)

          resolve(true)
        }
      }

      this.timer = requestAnimationFrame(fn)
    })
  }

  async view(config) {
    if (camera) {
      await this.rotateAnimate(
        this.lastRotateZ,
        config.rotateZ,
        this.lastRotateY,
        config.rotateY,
        this.lastDistance,
        config.distance,
        config.duration,
      )

      this.lastRotateZ = config.rotateZ
      this.lastRotateY = config.rotateY
      this.lastDistance = config.distance
    }
  }
}

使用的时候:

const viewAnimate = new ViewAnimate(scene, camera, cameraPosition)

// 视角切换序列,可以自行添加更多
viewAnimate
  .view({
    rotateZ: 90,
    rotateY: 180,
    distance: 0.5,
    duration: 1000,
  })
  .then(() =>
    viewAnimate.view({
      rotateZ: 0,
      rotateY: 0,
      distance: 1,
      duration: 1000,
    }),
  )
  .then(() =>
    viewAnimate.view({
      rotateZ: 30,
      rotateY: -120,
      distance: 2,
      duration: 1000,
    }),
  )
  .then(() =>
    viewAnimate.view({
      rotateZ: 0,
      rotateY: 0,
      distance: 1,
      duration: 1000,
    }),
  )

如果内容对您有一点点帮助,请给我一个赞吧!


xachary
1 声望0 粉丝

Be an entry-level front-end developer for a long time.