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="rpgCamera" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import 'babylonjs-loaders'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  FreeCamera,
  Vector3,
  StandardMaterial,
  Color3,
  Color4,
  DirectionalLight,
  MeshBuilder,
  OimoJSPlugin,
  PhysicsImpostor,
  Texture,
  Viewport,
  ShadowGenerator,
  DeviceType,
  DeviceSourceManager
} from 'babylonjs'
import {
  AdvancedDynamicTexture,
  StackPanel,
  Control,
  TextBlock,
} from 'babylonjs-gui'

let sceneResources

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

const speed = 0.1
let isJump = false

const isRightHandedSystem = 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("rpgCamera") as any

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

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

  const scene: any = new Scene(engine)
  // 使用左手坐标系
  scene.useRightHandedSystem = isRightHandedSystem

  const camera = new ArcRotateCamera('camera', 0, 0, 15, new Vector3(0, 0, 0), scene)
  
  
  const createAxis = () => {
    const axisX = MeshBuilder.CreateLines(
      'axisX', {
        colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
      },
      scene
    )

    const axisY = MeshBuilder.CreateLines(
      'axisY', {
        colors: [new Color4(0, 1, 0, 1),  new Color4(0, 1, 0, 1)  ],
        points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
      },
      scene
    )

    const axisZ = MeshBuilder.CreateLines(
      'axisZ', {
        colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
      },
      scene
    )

    return [axisX, axisY, axisZ]
  }

  const createGui = async () => {
    const adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')

    const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    xBox.position = new Vector3(80, 0, 0)
    const xPanel = new StackPanel()
    xPanel.width = '20px'
    xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const x = new TextBlock()
    x.text = 'X'
    x.height = '30px'
    x.color = 'red'
    adt.addControl(xPanel)
    xPanel.addControl(x)
    xPanel.linkWithMesh(xBox)

    const yBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    yBox.position = new Vector3(0, 80, 0)
    const yPanel = new StackPanel()
    yPanel.width = '20px'
    yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const y = new TextBlock()
    y.text = 'Y'
    y.height = '30px'
    y.color = 'green'
    adt.addControl(yPanel)
    yPanel.addControl(y)
    yPanel.linkWithMesh(yBox)

    const zBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    zBox.position = new Vector3(0, 0, 80)
    const zPanel = new StackPanel()
    zPanel.width = '20px'
    zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const z = new TextBlock()
    z.text = 'Z'
    z.height = '30px'
    z.color = 'blue'
    adt.addControl(zPanel)
    zPanel.addControl(z)
    zPanel.linkWithMesh(zBox)
  }

  const createLight = () => {
    const light = new DirectionalLight('dir01', new Vector3(0, -1, -0.3), scene)
    light.position = new Vector3(20, 60, 30)
    return light
  }

  const setScene = () => {
    scene.collesionEnabled = true
    scene.enablePhysics(
      new Vector3(0, -9.81, 0),
      new OimoJSPlugin()
    )
  }

  const setFirstCameraInfo = (firstCamera) => {
    firstCamera.attachControl(ele, true)

    firstCamera.applyGravity = true

    firstCamera.wheelPrecision = 30

    
    if (isRightHandedSystem) { // 右手坐标系
      firstCamera.setPosition(new Vector3(0, 20, 20))
    } else { // 左手坐标系
      firstCamera.setPosition(new Vector3(0, 20, -20))
    }

    // 最小缩放
    firstCamera.lowerRadiusLimit = 4

    // 最大缩放
    firstCamera.upperRadiusLimit = 20
    
    // 摄像机不能看到头顶
    // firstCamera.lowerBetaLimit = Math.PI / 6
    // firstCamera.upperBetaLimit = Math.PI / 2

    // 设置 camera.viewport = new Viewport(0, 0, 1, 1) 时,实际上是在告诉相机渲染整个画布
    firstCamera.viewport = new Viewport(0, 0, 1, 1)

    // 用于控制相机渲染哪些网格(meshes)的一个属性
    // 当多个相机同时激活时,layerMask 可以用来控制哪些网格应该被哪些相机渲染
    firstCamera.layerMask = 1

    // 会在出场以原来的角度为基础做偏转。
    // firstCamera.inertialBetaOffset = 0.01
    // firstCamera.inertialAlphaOffset = 0.05

    scene.activeCamera = firstCamera
    scene.activeCameras.push(firstCamera)
  }

  const createSecondCamera = () => {
    // 小地图相机,使用freeCamera作为测试
    // 如果地图面积很大,是不能显示全图的
    // 一般是把目标的position同步给secondCamera,跟随目标
    // 使用freeCamera,主要是查看到物体的jump效果
    const secondCamera = new FreeCamera('mini_map', new Vector3(0, 200, 0), scene)
    
    secondCamera.viewport = new Viewport(0, 0, 1, 1)

    secondCamera.layerMask = 2

    // 调整相机的位置
    secondCamera.setTarget(new Vector3(0, 0, 0))
    secondCamera.rotation.y = 0
    secondCamera.position.x = -100
    secondCamera.position.z = -32

    scene.activeCameras.push(secondCamera)

    return secondCamera
  }

  const createDeviceSourceManager = () => {
    // 添加键盘事件,控制人物行动
    const dsm = new DeviceSourceManager(engine)
    	// 添加检测事件
    dsm.onDeviceConnectedObservable.add(device => {
      console.log('device', device)
    })
    return dsm
  }

  const createGround = () => {
    const groundMat = new StandardMaterial('groundMat', scene)
    groundMat.diffuseTexture = new Texture('/images/wood.jpg')
    groundMat.specularColor = new Color3(0, 0, 0)
    groundMat.emissiveColor = new Color3(0.3, 0.3, 0.3)

    const ground = MeshBuilder.CreateGround('ground', {
      width: 100,
      height: 100,
      subdivisions: 4 // 表示地面被分割成多少个小正方形
    })
    ground.position.y = 0
    
    ground.material = groundMat
    ground.physicsImpostor = new PhysicsImpostor(ground, PhysicsImpostor.BoxImpostor, {
      mass: 0,
      friction: 0.0,
      restitution: 0 // 去掉碰撞反弹力
    }, scene)
    ground.receiveShadows = true
    ground.checkCollisions = true
    return ground
  }

  const createShadow = (light, mesh) => {
    const shadowGenerator: any = new ShadowGenerator(1024, light)
    shadowGenerator.addShadowCaster(mesh)
  }

  const createBox = (firstCamera) => {
    const box = MeshBuilder.CreateBox('box', {
      width: 2,
      height: 2,
      depth: 2
    }, scene)
    box.checkCollisions = true
    box.position.y = 0
    box.physicsImpostor = new PhysicsImpostor(box, PhysicsImpostor.BoxImpostor, {
      mass: 2,
      friction: 0.0, 
      restitution: 0 // 去掉碰撞反弹力
    }, scene)

    // 从高度落下
    box.position.y = 5

    const material = new StandardMaterial('material', scene)
    material.emissiveColor = new Color3(0, 0.58, 0.86)
    box.material = material

    // 如果在右手坐标系中,通过boxX、boxY、boxZ可以看出
    // box的x轴正方向指向左侧,boxZ的正方向指向前方
    const boxX = MeshBuilder.CreateLines("axisX", {  
        points: [new Vector3(0, 0, 0), new Vector3(3, 0, 0)],  
        colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)]  
    }, scene)
    boxX.parent = box
    const boxY = MeshBuilder.CreateLines("axisX", {  
        points: [new Vector3(0, 0, 0), new Vector3(0, 3, 0)],  
        colors: [new Color4(0, 1, 0, 1), new Color4(0, 1, 0, 1)]  
    }, scene)
    boxY.parent = box
    const boxZ = MeshBuilder.CreateLines("axisX", {  
        points: [new Vector3(0, 0, 0), new Vector3(0, 0, 3)],  
        colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)]  
    }, scene)
    boxZ.parent = box

   

    firstCamera.setTarget(box)
    return box
  }

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

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

  createAxis()
  createGui()
  const newLight = createLight()
  setScene()
  setFirstCameraInfo(camera)
  createSecondCamera()
  createGround()
  const box = createBox(camera)
  createShadow(newLight, box)
  const newDsm = createDeviceSourceManager()
  runAnimate()

  let upSpeed = 0
 
  scene.registerBeforeRender(() => {
    const firstCameraForwardRayPosition = camera.getForwardRay().direction
    const firstCameraForwardRayPositionWithoutY = new Vector3(firstCameraForwardRayPosition.x, 0, firstCameraForwardRayPosition.z)
    // // 物体的视角
    // // lookAt 方法的参数是一个目标位置,以及三个分别代表旋转的三个轴(X、Y、Z)的值。
    // // 在这里,后面的(0, 0, 0)分别代表绕X轴、Y轴和Z轴的旋转量,设置为0意味着不进行额外的旋转,只根据目标位置调整朝向。
    box.lookAt(box.position.add(firstCameraForwardRayPositionWithoutY), 0, 0, 0)
		
    const keyboard = newDsm.getDeviceSource(DeviceType.Keyboard)
    if (keyboard) {
      // 用于在本地坐标系(即对象的局部空间)中移动网格(Mesh)或其他变换节点(TransformNode)
      // locallyTranslate:移动 box 网格,沿 x 轴移动 x 单位,y 轴移动 y 单位,z 轴移动 z 单位
      if (keyboard.getInput(65) === 1) { // a
        const s = isRightHandedSystem ? -speed * -1 : -speed
        box.locallyTranslate(new Vector3(s, 0, 0))
      } else if (keyboard.getInput(87) === 1) { // w
        box.locallyTranslate(new Vector3(0, 0, speed))
      } else if (keyboard.getInput(83) === 1) { // s
        box.locallyTranslate(new Vector3(0, 0, -speed))
      } else if (keyboard.getInput(68) === 1) { // d
        const s = isRightHandedSystem ? speed * -1 : speed
        box.locallyTranslate(new Vector3(s, 0, 0))
      } else if (keyboard.getInput(74) === 1 && !isJump) { // j
        isJump = true
        box?.physicsImpostor?.applyImpulse(
          new Vector3(0, 5, 0),
          box.getAbsolutePosition()
        )
      } else if (keyboard.getInput(70) === 1) { // f
        // mesh.physicsImpostor.applyImpulse 方法用于向物理对象(在此例中为一个盒子,即 box)施加一个瞬时冲量(impulse)。
        // 这个冲量可以改变对象的速度,从而模拟现实世界中的力对物体的影响
        // 参数1、冲量向量,是一个 Vector3 对象,表示要施加的冲量的方向和大小。冲量是一个矢量,因此它既有大小(力的大小乘以时间),又有方向。
        // 参数2、世界坐标中的点,是一个 Vector3 对象,表示冲量应用在世界坐标中的哪个点上。
        upSpeed += 0.01
        box?.physicsImpostor?.applyImpulse(
          new Vector3(0, upSpeed, 0),
					box.getAbsolutePosition()
        )
      
      } else {
        if (box.position.y < 1) { // 用来限制防止jump的时候多次触发
          upSpeed = 0
          isJump = false
        }
      }
    }
  })

     
  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>

多物体选择

按“Z"来锁住/解锁视角
fps: 0
点击运行
<template>
  <div>
    <div>
      <div>按“Z"来锁住/解锁视角</div>
      <div class="flex space-between">
        <div>fps: {{ fps }}</div>
        <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
      </div>
    </div>
    <div style="position: relative;">
      <div id="customBox">
        <canvas v-if="isRunning" id="rpgMultiObjectSelection" class="stage"></canvas>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import 'babylonjs-loaders'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  StandardMaterial,
  Color3,
  Color4,
  DirectionalLight,
  MeshBuilder,
  Matrix,
  PointerEventTypes
} from 'babylonjs'
import {
  AdvancedDynamicTexture,
  StackPanel,
  Control,
  TextBlock,
} from 'babylonjs-gui'

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

const isRightHandedSystem = false
const boxList = ref<any>([])
const createDiv = ref<any>(null)
const pointerDown = ref(false)
const startPoint = ref<any>({})
const endPoint = ref<any>({})
const collectionMesh = ref<any>([])
const saveMaterial = ref<any>([])

let sceneResources

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("rpgMultiObjectSelection") as any
  const customBox = document.getElementById("customBox") as any
  const boxWidth = Number(window.getComputedStyle(customBox).width.split('px')[0])
  const boxHeight = Number(window.getComputedStyle(customBox).height.split('px')[0])

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

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

  const scene: any = new Scene(engine)
  // 使用左手坐标系
  scene.useRightHandedSystem = isRightHandedSystem

  const camera = new ArcRotateCamera('camera', 0, 0, 15, new Vector3(0, 0, 0), scene)
  camera.upperBetaLimit = Math.PI / 2.2
  camera.wheelPrecision = 30
  camera.panningSensibility = 200
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(25, 25, 25))
  
  const createAxis = () => {
    const axisX = MeshBuilder.CreateLines(
      'axisX', {
        colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
      },
      scene
    )

    const axisY = MeshBuilder.CreateLines(
      'axisY', {
        colors: [new Color4(0, 1, 0, 1),  new Color4(0, 1, 0, 1)  ],
        points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
      },
      scene
    )

    const axisZ = MeshBuilder.CreateLines(
      'axisZ', {
        colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
      },
      scene
    )

    return [axisX, axisY, axisZ]
  }

  const createGui = async () => {
    const adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')

    const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    xBox.position = new Vector3(80, 0, 0)
    const xPanel = new StackPanel()
    xPanel.width = '20px'
    xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const x = new TextBlock()
    x.text = 'X'
    x.height = '30px'
    x.color = 'red'
    adt.addControl(xPanel)
    xPanel.addControl(x)
    xPanel.linkWithMesh(xBox)

    const yBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    yBox.position = new Vector3(0, 80, 0)
    const yPanel = new StackPanel()
    yPanel.width = '20px'
    yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const y = new TextBlock()
    y.text = 'Y'
    y.height = '30px'
    y.color = 'green'
    adt.addControl(yPanel)
    yPanel.addControl(y)
    yPanel.linkWithMesh(yBox)

    const zBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    zBox.position = new Vector3(0, 0, 80)
    const zPanel = new StackPanel()
    zPanel.width = '20px'
    zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const z = new TextBlock()
    z.text = 'Z'
    z.height = '30px'
    z.color = 'blue'
    adt.addControl(zPanel)
    zPanel.addControl(z)
    zPanel.linkWithMesh(zBox)
  }

  const createLight = () => {
    const light = new DirectionalLight('dir01', new Vector3(0, -1, -0.3), scene)
    light.position = new Vector3(20, 60, 30)
    return light
  }

  const createBox = () => {
    for (let i = 0; i < 10; i++) {
      const box = MeshBuilder.CreateBox(`box${i}`, {
        size: 1
      }, scene)
      const material = new StandardMaterial(`material${i}`, scene)
      const r = Math.random()
      const g = Math.random()
      const b = Math.random()
      material.emissiveColor = new Color3(r, g, b) // 设置颜色属性
      const x = Math.random() * 10 + i
      const y = Math.random() * 10 + i
      const z = Math.random() * 10 + i
      box.position = new Vector3(x, y, z)
      box.material = material
      boxList.value.push(box)
    }
  }

  const createDivPlane = () => {
    createDiv.value = document.createElement('div')
    createDiv.value.style.pointerEvents = 'none'
    createDiv.value.style.border = '1px solid red'
    createDiv.value.style.background = 'rgba(222, 160, 131, 0.2)'
    createDiv.value.style.position = 'absolute'
    createDiv.value.style.display = 'none'
    createDiv.value.style.left = '0'
    createDiv.value.style.top = '0'
    createDiv.value.style.zIndex = '10'
    createDiv.value.style.transformOrigin = '0 0'
    customBox.appendChild(createDiv.value)
  }

  /**
   * 世界坐标转成屏幕坐标
   */
  const convertWorldCoordinates2ScreenCoordinates = mesh => {
    // defines the Vector3 to project
    const meshPos = mesh.position
    // defines the world matrix to use
    const identity = Matrix.Identity()
    // defines the transform (view x projection) matrix to use
    const sceneMatrix = scene.getTransformMatrix()
    // defines the screen viewport to use
    const cameraViewport = camera.viewport

    const screenCoordinates = Vector3.Project(
      meshPos,
      identity,
      sceneMatrix,
      cameraViewport
    )
    

    const x = screenCoordinates.x * boxWidth
    const y = screenCoordinates.y * boxHeight

    return {
      x,
      y
    }
  }

  /** 
   * 判断是否在框选中
   */
  const isInRectangle = (rectangle, point) => {
    const { sx, sy, ex, ey } = rectangle
    const { x, y } = point
    if (sx < x && x < ex && sy < y && y < ey) return true
    return false
  }

  const fixedNumb = n => {
    return Number(n.toFixed(4))
  }

  const pointerDownFun = e => {
    if (e.type === PointerEventTypes.POINTERDOWN && isLockScreen.value) {
      pointerDown.value = true

      startPoint.value = {
        x: fixedNumb(e.event.offsetX),
        y: fixedNumb(e.event.offsetY)
      }

      for (let i = 0; i < collectionMesh.value.length; i++) {
        collectionMesh.value[i].material.emissiveColor = saveMaterial.value[i]
      }

      collectionMesh.value = []
      saveMaterial.value = []

      createDiv.value.style.transform = `translate3d(${startPoint.value.x}px, ${startPoint.value.y}px, 0) rotate3d(0, 0, 0, 180deg)`
    }
  }

  const pointerMoveFun = e => {
    if (e.type === PointerEventTypes.POINTERMOVE && pointerDown.value && isLockScreen.value) {
      endPoint.value = {
        x: fixedNumb(e.event.offsetX),
        y: fixedNumb(e.event.offsetY)
      }


      const width = fixedNumb(endPoint.value.x - startPoint.value.x)
      const height = fixedNumb(endPoint.value.y - startPoint.value.y)

      let rX = 0
      let rY = 0
      let rZ = 0
      if (width < 0 && height > 0) {
        rY = 1
      } else if (width > 0 && height < 0) {
        rX = 1
      } else if (width < 0 && height < 0) {
        rZ = 1
      }

      createDiv.value.style.display = width === 0 || height === 0 ? 'none' : 'block'
      createDiv.value.style.transform = `translate3d(${startPoint.value.x}px, ${startPoint.value.y}px, 0) rotate3d(${rX}, ${rY}, ${rZ}, 180deg)`
      createDiv.value.style.width = Math.abs(width) + 'px'
      createDiv.value.style.height = Math.abs(height) + 'px'
    }
  }

  const pointerUpFun = e => {
    if (e.type === PointerEventTypes.POINTERUP && isLockScreen.value) {
      pointerDown.value = false

      endPoint.value = {
        x: fixedNumb(e.event.offsetX),
        y: fixedNumb(e.event.offsetY)
      }

      boxList.value.forEach(mesh => {
        const sx = Math.min(startPoint.value.x, endPoint.value.x)
        const sy = Math.min(startPoint.value.y, endPoint.value.y)
        const ex = Math.max(startPoint.value.x, endPoint.value.x)
        const ey = Math.max(startPoint.value.y, endPoint.value.y)
        const {
          x,
          y
        } = convertWorldCoordinates2ScreenCoordinates(mesh)
        const bool = isInRectangle({
          sx,
          sy,
          ex,
          ey
        }, {
          x,
          y
        })
        if (bool) collectionMesh.value.push(mesh)
      })

      createDiv.value.style.width = 0 + 'px'
      createDiv.value.style.height = 0 + 'px'
      createDiv.value.style.display = 'none'

      for (let i = 0; i < collectionMesh.value.length; i++) {
        saveMaterial.value.push(collectionMesh.value[i].material.emissiveColor.clone())
        collectionMesh.value[i].material.emissiveColor = Color3.Black()
      }
    }
  }


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

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

  scene.onPointerObservable.add(e => {
    pointerDownFun(e)
    pointerMoveFun(e)
    pointerUpFun(e)
  })

  createAxis()
  createGui()
  createLight()
  createBox()
  createDivPlane()
  runAnimate()


     
  return {
    scene,
    engine,
    camera
  }
}

const lockScreen = (e) => {
  if (isRunning.value) {
    if (e.key.toLocaleLowerCase() === 'z' && !isLockScreen.value) {
      isLockScreen.value = true
      sceneResources.camera.detachControl()
    } else if (e.key.toLocaleLowerCase() === 'z' && isLockScreen) {
      isLockScreen.value = false
      sceneResources.camera.attachControl()
    }
  }
}

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

onMounted(async() => {
  await nextTick()
  window.addEventListener('keydown', lockScreen)
})

onUnmounted(() => {
  destroy()
  window.removeEventListener('keydown', lockScreen)
})
</script>

人物前进、后退、舞蹈

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

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import 'babylonjs-loaders'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  StandardMaterial,
  Color3,
  Color4,
  DirectionalLight,
  MeshBuilder,
  CubeTexture,
  Texture,
  ActionManager,
  ExecuteCodeAction,
  ImportMeshAsync
} from 'babylonjs'
import {
  AdvancedDynamicTexture,
  StackPanel,
  Control,
  TextBlock
} from 'babylonjs-gui'

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

const inputMap = {}
const isRightHandedSystem = false
let sceneResources

// 角色变量
const peopleSpeed = 0.2
const peopleSpeedBackwards = 0.06
const peopleRotationSpeed = 0.01
let animating = true
let walkAnim 
let walkBackAnim
let idleAnim
let sambaAnim

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("rpgDance") as any

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

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

  const scene: any = new Scene(engine)
  // 使用左手坐标系
  scene.useRightHandedSystem = isRightHandedSystem

  const camera = new ArcRotateCamera('camera', 0, 0, 15, new Vector3(0, 0, 0), scene)
  camera.upperBetaLimit = Math.PI / 2.2
  camera.wheelPrecision = 30
  camera.panningSensibility = 200
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(25, 25, 25))
  
  const createAxis = () => {
    const axisX = MeshBuilder.CreateLines(
      'axisX', {
        colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
      },
      scene
    )

    const axisY = MeshBuilder.CreateLines(
      'axisY', {
        colors: [new Color4(0, 1, 0, 1),  new Color4(0, 1, 0, 1)  ],
        points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
      },
      scene
    )

    const axisZ = MeshBuilder.CreateLines(
      'axisZ', {
        colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
      },
      scene
    )

    return [axisX, axisY, axisZ]
  }

  const createGui = async () => {
    const adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')

    const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    xBox.position = new Vector3(80, 0, 0)
    const xPanel = new StackPanel()
    xPanel.width = '20px'
    xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const x = new TextBlock()
    x.text = 'X'
    x.height = '30px'
    x.color = 'red'
    adt.addControl(xPanel)
    xPanel.addControl(x)
    xPanel.linkWithMesh(xBox)

    const yBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    yBox.position = new Vector3(0, 80, 0)
    const yPanel = new StackPanel()
    yPanel.width = '20px'
    yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const y = new TextBlock()
    y.text = 'Y'
    y.height = '30px'
    y.color = 'green'
    adt.addControl(yPanel)
    yPanel.addControl(y)
    yPanel.linkWithMesh(yBox)

    const zBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    zBox.position = new Vector3(0, 0, 80)
    const zPanel = new StackPanel()
    zPanel.width = '20px'
    zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const z = new TextBlock()
    z.text = 'Z'
    z.height = '30px'
    z.color = 'blue'
    adt.addControl(zPanel)
    zPanel.addControl(z)
    zPanel.linkWithMesh(zBox)
  }

  const createLight = () => {
    const light = new DirectionalLight('dir01', new Vector3(0, -1, -0.3), scene)
    light.position = new Vector3(20, 60, 30)
    return light
  }

  const createSkyBox = () => {
    const skybox = MeshBuilder.CreateBox('skyBox', { size: 150 }, scene)
    const skyboxMaterial = new StandardMaterial('skyBox', scene)
    skyboxMaterial.backFaceCulling = false
    skyboxMaterial.reflectionTexture = new CubeTexture('/images/skyBox2', scene)
    skyboxMaterial.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE
    skyboxMaterial.diffuseColor = new Color3(0, 0, 0)
    skyboxMaterial.specularColor = new Color3(0, 0, 0)
    skybox.material = skyboxMaterial
  }

  const createGround = () => {
    const ground = MeshBuilder.CreateGround('ground', {
      height: 150,
      width: 150,
      subdivisions: 150
    }, scene)
    const groundMaterial: any = new StandardMaterial('groundMaterial', scene)
    groundMaterial.diffuseTexture = new Texture('/images/wood.jpg', scene)
    groundMaterial.diffuseTexture.uScale = 30
    groundMaterial.diffuseTexture.vScale = 30
    groundMaterial.specularColor = new Color3(0.1, 0.1, 0.1)
    ground.material = groundMaterial
  }

  const createGuiTip = () => {
    const adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')
    const instructions = new TextBlock()
    instructions.text = 'Move W,A,S,D keys, B for Samba, look with the mouse'
    instructions.color = 'white'
    instructions.fontSize = 16
    instructions.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    instructions.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    adt.addControl(instructions)
  }

  const createActionManager = () => {
    scene.actionManager = new ActionManager(scene)
    scene.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnKeyDownTrigger, function(evt) {
      inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown'
    }))
    scene.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnKeyUpTrigger, function(evt) {
      inputMap[evt.sourceEvent.key] = evt.sourceEvent.type === 'keydown'
    }))
  }

  const loaderPeople = async () => {
    const module = await ImportMeshAsync('/scenes/HVGirl.glb', scene)
    // meshes第0是root
    const people = module.meshes[0]

    // 将相机锁定在角色上
    camera.setTarget(people)

    // 缩小模型
    people.scaling.scaleInPlace(0.5)

    walkAnim = scene.getAnimationGroupByName('Walking')
    walkBackAnim = scene.getAnimationGroupByName('WalkingBack')
    idleAnim = scene.getAnimationGroupByName('Idle')
    sambaAnim = scene.getAnimationGroupByName('Samba')

    return people
  }

  const peopleAction = (people) => {
    let keyDown = false
    // 管理角色的动作,例如位置、方向
    if (inputMap['w']) {
      // 尝试移动mesh,并检测碰撞
      people.moveWithCollisions(people.forward.scaleInPlace(peopleSpeed))
      keyDown = true
    }
    if (inputMap['s']) {
      people.moveWithCollisions(people.forward.scaleInPlace(-peopleSpeedBackwards))
      keyDown = true
    }
    if (inputMap['a']) {
      people.rotate(Vector3.Up(), -peopleRotationSpeed)
      keyDown = true
    }
    if (inputMap['d']) {
      people.rotate(Vector3.Up(), peopleRotationSpeed)
      keyDown = true
    }
    if (inputMap['b']) {
      keyDown = true
    }

    // 管理要播放的动画
    if (keyDown) {
      if (!animating) {
        animating = true
        if (inputMap['s']) {
          // 倒着走
          walkBackAnim.start(true, 1.0, walkBackAnim.from, walkBackAnim.to, false)
        } else if (inputMap['b']) {
          // Samba!
          sambaAnim.start(true, 1.0, sambaAnim.from, sambaAnim.to, false)
        } else {
          // Walk
          walkAnim.start(true, 1.0, walkAnim.from, walkAnim.to, false)
        }
      }
    } else {
      if (animating) {
        // 没有按键按下时默认动画是空闲的
        idleAnim.start(true, 1.0, idleAnim.from, idleAnim.to, false)
        // 没有按键按下时停止除 idleAnim 之外的所有动画
        sambaAnim.stop()
        walkAnim.stop()
        walkBackAnim.stop()
        // 确保每个渲染循环只播放一次动画
        animating = false
      }
    }
    
  }
  
  const runAnimate = () => {
    engine.runRenderLoop(function() {
      if (scene && scene.activeCamera) {
        scene.render()

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


  createAxis()
  createGui()
  createLight()
  createSkyBox()
  createGround()
  createGuiTip()
  createActionManager()
  const people = await loaderPeople()

  
  runAnimate()

  scene.onBeforeRenderObservable.add(() => {
    peopleAction(people)
  })
     
  return {
    scene,
    engine,
    camera
  }
}

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

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

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