Skip to content

开门关门 + 相机跟随

fps: 0
点击运行
<template>
  <div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <canvas v-if="isRunning" id="animationDoorCamera" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Engine,
  Scene,
  UniversalCamera,
  Vector3,
  HemisphericLight,
  MeshBuilder,
  StandardMaterial,
  Color3,
  SpotLight,
  Animation
} from 'babylonjs'

let sceneResources

const fps = ref(0)
const isRunning = ref(false)

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = await initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const initScene = async () => {
  const ele = document.getElementById("animationDoorCamera") as any

  ele.addEventListener('wheel', function(event) {
    // 根据需要处理滚动
    // 例如,可以修改相机的半径或角度
    event.preventDefault() // 阻止默认滚动行为
  })

  const engine: any = new Engine(ele, true, {
    preserveDrawingBuffer: true,
    stencil: true,
    disableWebGL2Support: false
  })

  const scene = new Scene(engine)
  scene.useRightHandedSystem = true

  const camera =  new UniversalCamera('UniversalCamera', new Vector3(0, 3, -30), scene)

  const createLight = () => {
    const light1 = new HemisphericLight('light',new Vector3(0, -1, 0), scene)
    light1.intensity = 0.25

    const light2 = new HemisphericLight('light',new Vector3(0, 1, -1), scene)
    light2.intensity = 0.5
  }


  /** 创建墙体 */
  const createBackground = () => {
    const ground = MeshBuilder.CreateGround('ground', {
      width: 50, height: 50
    }, scene)
    const mat = new StandardMaterial('sourceMat', scene)
    mat.diffuseColor = new Color3(0, 1, 1)
    ground.material = mat

    const wallList = [
      { width: 8, height: 6, depth: 0.1, position: { x: -6, y: 3 } },
      { width: 4, height: 6, depth: 0.1, position: { x: 2, y: 3 } },
      { width: 2, height: 2, depth: 0.1, position: { x: -1, y: 5 } },
      { width: 14, height: 6, depth: 0.1, position: { x: -3, y: 3, z: 7 } },
      { width: 7, height: 6, depth: 0.1, position: { x: -10, y: 3, z: 3.5 }, rotation: { y: Math.PI / 2 } },
      { width: 7, height: 6, depth: 0.1, position: { x: 3, y: 3, z: 3.5 }, rotation: { y: Math.PI / 2 } },
      { width: 7, height: 6, depth: 0.1, position: { x: 4, y: -3, z: 3.5 }, rotation: { y: Math.PI / 2 } },
    ]
    for (let i = 0; i < wallList.length; i++) {
      const cur = wallList[i]
      const wall = MeshBuilder.CreateBox('wall' + i, {
        width: cur.width, height: cur.height, depth: cur.depth,
      }, scene)
      for (const key in cur.position) {
        wall.position[key] = cur.position[key]
      }
      if (cur.rotation) {
        for (const key in cur.rotation) {
          wall.rotation[key] = cur.rotation[key]
        }
      }
    }
  }


  /** 动画相关 */

  const frameRate = 20
  const lightPositions = [-2, 3, 6.9]
  const lightDirections = [-0.5, -0.25, 1, 0, 0, -1]
  
  let sphereList: any = []
  let spotLights: any = []

  /** 创建门 */
  const createDoor = () => {
    const door = MeshBuilder.CreateBox('door', {
      width: 2, height: 4, depth: 0.1
    }, scene)

    const mat = new StandardMaterial('sourceMat', scene)
    mat.diffuseColor = new Color3(1, 0, 1)

    door.material = mat

    return door
  }

  /** 创建枢纽 */
  const createHinge  = (door) => {
    const hinge = MeshBuilder.CreateBox('hinge', {}, scene)
    hinge.isVisible = false
    door.parent = hinge
    hinge.position.y = 2
    door.position.x = -1
    return hinge
  }

  /** 创建球体 */
  const createSphere = () => {
    const sphereLight: any = MeshBuilder.CreateSphere('sphere', {
      diameter: 0.2
    }, scene)
    sphereLight.material = new StandardMaterial('', scene)
    sphereLight.material.emissiveColor = new Color3(1, 1, 1)
    sphereLight.position.x = 2
    sphereLight.position.y = 3
    sphereLight.position.z = 0.1
    return sphereLight
  }

  /** 设置多个球体的数组 */
  const setSphereList = (sphere) => {
    sphereList.push(sphere)

    sphereList.push(sphere.clone())
    sphereList[1].position = new BABYLON.Vector3(
      lightPositions[0],
      lightPositions[1],
      lightPositions[2]
    )
  }

  /** 球体上的的灯 */
  const createSpotLight = (sphereList) => {
    for (let i = 0; i < sphereList.length; i++) {
      spotLights[i] = new SpotLight(
        'spotLight' + i, 
        sphereList[i].position, 
        new Vector3(
          lightDirections[3 * i],
          lightDirections[3 * i + 1],
          lightDirections[3 * i + 2]
        ),
        Math.PI / 8,
        5,
        scene
      )
      spotLights[i].diffuse = new Color3(1, 1, 1)
      spotLights[i].specular = new Color3(0.5, 0.5, 0.5)
      spotLights[i].intensity = 0
    }
  }

  /** 动画——让相机扫一圈 */ 
  const createCameraRotate = () => {
    // frame: 表示动画时间轴上的关键帧位置,通常以帧为单位。在这个例子中,0 表示动画的起始帧。
    // value: 表示在该帧上,被动画化属性的值。在这个例子中,0 表示该属性在动画开始时的初始值。
    const rotateKeys = [
      { frame: 0, value: 0 },
      { frame: 9 * frameRate, value: 0 },
      { frame: 14 * frameRate, value: Math.PI }
    ]
    const rotate = new Animation(
      'rotate',
      'rotation.y',
      frameRate,
      Animation.ANIMATIONTYPE_FLOAT,
      Animation.ANIMATIONLOOPMODE_CONSTANT
    )
    rotate.setKeys(rotateKeys)
    return rotate
  }

  /** 
   * 让相机移动
   * 移动相机会改变相机的位置(vector3)。相机环顾四周是围绕 y 轴(float)的旋转。
   * 由于动画只能更改一个属性,因此相机需要两个动画。
   * 移动摄像机的关键点将在时间 0 开始,摄像机将开始远离建筑物并上下移动,直到它在 3 秒的时间刚好在门外。
   * 当门打开时,摄像机将保持其位置 2 秒,然后在 5 秒时将向前移动到与门成一定角度的房间,在 8 秒时停止。
   * 相机在 9 秒时不旋转,然后相机将在 14 秒时旋转 180 度以面对门。
   * 相机的关键点的值将是它在 0、 3、 5 和 8 秒处的帧中的位置以及它在 0、 9 和 14 秒处的帧中的旋转。
   */
  const createCameraMove = () => {
    const moveKeys = [
      { frame: 0, value: new Vector3(0, 5, -30) },
      { frame: 3 * frameRate, value: new Vector3(0, 2, -10) },
      { frame: 5 * frameRate, value: new Vector3(0, 2, -10) },
      { frame: 8 * frameRate, value: new Vector3(-2, 2, 3) }
    ]
    const move = new Animation(
      'move',
      'position',
      frameRate,
      Animation.ANIMATIONTYPE_VECTOR3,
      Animation.ANIMATIONLOOPMODE_CONSTANT
    )
    move.setKeys(moveKeys)
    return move
  }

  /** 
   * 用于打开和关闭门
   * 门以 y 轴为轴围绕铰链进行旋转。打开和关闭门的旋转将分别需要 2 秒。
   * 关键点是 0、 3、 5、 13 和 15 秒。
   * 关键点的值将是它在关键点处的帧中围绕 y 轴的旋转。
   */
  const createCameraSweep = () => {
    const sweepKeys = [
      { frame: 0, value: 0 },
      { frame: 3 * frameRate, value: 0 },
      { frame: 5 * frameRate, value: Math.PI / 3 },
      { frame: 13 * frameRate, value: Math.PI / 3 },
      { frame: 15 * frameRate, value: 0 },
    ]
    const sweep = new Animation(
      'sweep',
      'rotation.y',
      frameRate,
      Animation.ANIMATIONTYPE_FLOAT,
      Animation.ANIMATIONLOOPMODE_CONSTANT
    )
    sweep.setKeys(sweepKeys)
    return sweep
  }

  /**
   * 让光线变亮变暗
   * 灯光的强度会有所不同。这需要一系列聚光灯的数组。
   * 灯的关键点是保持关闭到 7 秒,在 10 秒达到全强度,直到 14 秒熄灭。
   */
  const createLightDimmer = () => {
    const dimmerKeys = [
      { frame: 0, value: 0 },
      { frame: 7 * frameRate, value: 0 },
      { frame: 10 * frameRate, value: 1 },
      { frame: 14 * frameRate, value: 1 },
      { frame: 15 * frameRate, value: 0 },
    ]
    const lightDimmer = new Animation(
      'dimmer',
      'intensity',
      frameRate,
      BABYLON.Animation.ANIMATIONTYPE_FLOAT,
      BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
    )
    lightDimmer.setKeys(dimmerKeys)
    return lightDimmer
  }

  /** 运行剪辑 */
  const createRunClip = (rotate, move, hinge, sweep, lightDimmer) => {
    scene.beginDirectAnimation(camera, [rotate, move], 0, 25 * frameRate, false)
    scene.beginDirectAnimation(hinge, [sweep], 0, 25 * frameRate, false)
    scene.beginDirectAnimation(spotLights[0], [lightDimmer], 0, 25 * frameRate, false)
  	scene.beginDirectAnimation(spotLights[1], [lightDimmer.clone()], 0, 25 * frameRate, false)
  }

  const runAnimate = () => {
    engine.runRenderLoop(function() {
      if (scene && scene.activeCamera) {
        scene.render()

        fps.value = engine.getFps().toFixed(2)
      }
    })
  }


  createLight()

  const sphere = createSphere()
  setSphereList(sphere)
  createSpotLight(sphereList)

  const door = createDoor()
  const hinge = createHinge(door)

  const rotate = createCameraRotate()

  const move = createCameraMove()

  const sweep = createCameraSweep()

  const lightDimmer = createLightDimmer()

  createRunClip(rotate, move, hinge, sweep, lightDimmer)
  createBackground()

  runAnimate()
  
  return {
    scene,
    engine, 
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.engine.stopRenderLoop() 
    sceneResources.engine.dispose()
    sceneResources.scene.dispose()
    sceneResources = null
  }
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>