動態

詳情 返回 返回

使用Three.js如何通過代碼動態改變模型的視角 - 動態 詳情

場景

作為剛剛接觸 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,
    }),
  )

如果內容對您有一點點幫助,請給我一個贊吧!

Add a new 評論

Some HTML is okay.