教你Vue3+Vite+threejs 创建3D场景,实现移动跳跃碰撞检测(2)

4,023 阅读3分钟

使用threejs实现的场景的创建,模型加载,移动,碰撞检测

为了让大家更容易看明白,这篇文章尽量更多的文字说明

上篇文章地址:(教你Vue3+Vite+threejs 创建3D场景,实现移动跳跃碰撞检测(1) #掘金文章# juejin.cn/post/718138…)

上篇实现了场景模型的加载,以及移动

实现方法:通过计算player的速度(vector3),以及时间片段,计算在每个时间片段的位移距离,来控制player在每一帧的位移,同时控制摄像机的位置与player位置同步,

// 更新位置
function updatePlayer(deltaTime) {

  let damping = Math.exp(- 4 * deltaTime) - 1;

  player.velocity.addScaledVector(player.velocity, damping);
  // console.log(player.velocity);
  const deltaPosition = player.velocity.clone().multiplyScalar(deltaTime);
  
  player.geometry.translate(deltaPosition);

  camera.position.copy(player.geometry.end);

}

这篇主要实现碰撞检测

第一步:跳跃的实现

我们需要一个变量playerOnFloor,来判断player是否在地面上 我们需要一个常量GEAVITY,描述引力大小

let playerOnFloor = false
let GRAVITY = 30 

在函数handleControls里面增加空格事件

速度根据playerOnFloor重新判断

以及,增加空格跳跃事件,如果在地上,则摁下空格添加一个Y轴初速度

// 根据摁下键盘控制

    const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);


  if (playerOnFloor) {

    if (keyStates['Space']) {

      player.velocity.y = 15;

    }

  }

这里结束已经可以实现跳跃,可以把playerOnFloor改成true测试一下,发现是有效的,但是跳起后没有落地,因为我们还没有实现重力效果。 看完效果我们把playerOnFloor改成false

第二步:重力效果的实现

我们在函数updatePlayer中实现重力效果

如果 playerOnFloor == false,给player在Y轴的速度递减

  if (!playerOnFloor) {
    player.velocity.y -= GRAVITY*deltaTime
    damping *= 0.1
  }

至此,重力效果也实现了,是不是很简单

第三步:碰撞检测的实现

碰撞检测算是一个难点,这里使用的是octree(八叉树)来检测碰撞,所以我们需要在全局引入Octree。并且在模型加载完成之后我们就可以调用worldOctree.fromGraphNode(gltf.scene) 并且添加OctreeHeiper

至于Octree是如何做碰撞检测的,为什么可以,原理是什么? 我也不懂,百度上的文章少之又少,只知道是一种数据结构。

  1. 引入模型的时候,我们就可以调用八叉树
import { Octree } from 'three/examples/jsm/math/Octree';
import { OctreeHelper } from 'three/examples/jsm//helpers/OctreeHelper.js';

// 八叉树
const worldOctree = new Octree()

function loadModel() {
  const loader = new GLTFLoader()
  loader.load('./models/collision-world.glb', gltf => {
    scene.add(gltf.scene)
    
    // Octree
    worldOctree.fromGraphNode(gltf.scene)

    // OctreeHelper
    const helper = new OctreeHelper(worldOctree);
    helper.visible = true;
    scene.add(helper);

    gltf.scene.traverse(child => {

      if (child.isMesh) {
        // 阴影效果
        child.castShadow = true;
        child.receiveShadow = true;

      }

    });
  })

我们可以通过OctreeHelper 看到效果 。

image.png
  1. 函数updatePlayer 中做碰撞检测

function updatePlayer(deltaTime) {

  let damping = Math.exp(- 4 * deltaTime) - 1;

  // 如果人物不在地面上,人物Y轴速度根据GRAVITY来递减
  if (!playerOnFloor) {

    playerVelocity.y -= GRAVITY * deltaTime;

    // small air resistance
    damping *= 0.1;

  }

  playerVelocity.addScaledVector(playerVelocity, damping);

  const deltaPosition = playerVelocity.clone().multiplyScalar(deltaTime);
  playerCollider.translate(deltaPosition);

  playerCollisions();  // 碰撞检测的位置也很重要,一定要在player移动之后检查

  persCamera.position.copy(playerCollider.end);

}

playerCollisions()

const result = worldOctree.capsuleIntersect(playerCollider)

如果没有碰撞,函数返回false , 如果碰撞了,函数返回一个对象,包含碰撞点信息,根据信息重新计算player的速度

function playerCollision() {
  // 碰撞检测
  const result = worldOctree.capsuleIntersect(player.geometry)
  
  playerOnFloor = false
  // 有碰撞,result对象包含了碰撞点的信息
  if (result) {
    playerOnFloor = result.normal.y > 0
    if (!playerOnFloor) {
      // 重新计算 player的速度向量
      player.velocity.addScaledVector(result.normal, -result.normal.dot(player.velocity))
    }
    player.geometry.translate(result.normal.multiplyScalar(result.depth));
  }
}

最后,我们终于实现了官方文档的部分效果,模型加载,移动,跳跃,碰撞检测

Rec1111.GIF

完整代码

<template>
  <div id="container"></div>
</template>

<script setup>
import { onMounted } from '@vue/runtime-core';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { Capsule } from 'three/examples/jsm/math/Capsule'
import { Octree } from 'three/examples/jsm/math/Octree';
import { OctreeHelper } from 'three/examples/jsm//helpers/OctreeHelper.js';
import * as THREE from 'three'

let renderer, camera, scene, container;
let clock;
let keyStates = {}
let GRAVITY = 30
let playerOnFloor = true
const worldOctree = new Octree()

let player = {
  geometry: new Capsule(new THREE.Vector3(0, 0.35, 0), new THREE.Vector3(0, 1, 0), 0.35),
  velocity: new THREE.Vector3(),
  direction: new THREE.Vector3()
}
onMounted(() => {
  init()
})

function init() {
  // renderer
  container = document.getElementById('container');
  renderer = new THREE.WebGLRenderer({ antialias: true })  // antialias:true 开启抗锯齿
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(window.innerWidth, window.innerHeight)
  renderer.shadowMap.enable = true
  renderer.shadowMap.type = THREE.VSMShadowMap;
  renderer.outputEncoding = THREE.sRGBEncoding;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  container.appendChild(renderer.domElement);

  // camera
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
  camera.position.set(0, 0, 0)
  camera.rotation.order = 'YXZ'

  // scene
  scene = new THREE.Scene()
  scene.background = new THREE.Color(0x88ccee)
  scene.fog = new THREE.Fog(0x88ccee, 0, 50)

  // light
  const fillLight1 = new THREE.HemisphereLight(0x4488bb, 0x002244, 0.5);
  fillLight1.position.set(2, 1, 1);
  scene.add(fillLight1);

  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
  directionalLight.position.set(- 5, 25, - 1);
  directionalLight.castShadow = true;
  directionalLight.shadow.camera.near = 0.01;
  directionalLight.shadow.camera.far = 500;
  directionalLight.shadow.camera.right = 30;
  directionalLight.shadow.camera.left = - 30;
  directionalLight.shadow.camera.top = 30;
  directionalLight.shadow.camera.bottom = - 30;
  directionalLight.shadow.mapSize.width = 1024;
  directionalLight.shadow.mapSize.height = 1024;
  directionalLight.shadow.radius = 4;
  directionalLight.shadow.bias = - 0.00006;
  scene.add(directionalLight);

  // clock 
  clock = new THREE.Clock()

  window.addEventListener("resize", onResize)

  loadModel()
}
function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
}
// 加载模型
function loadModel() {
  const loader = new GLTFLoader()
  loader.load('./models/collision-world.glb', gltf => {
    scene.add(gltf.scene)

    // Octree
    worldOctree.fromGraphNode(gltf.scene)

    // OctreeHelper
    const helper = new OctreeHelper(worldOctree);
    helper.visible = false;
    scene.add(helper);

    gltf.scene.traverse(child => {

      if (child.isMesh) {
        // 阴影效果
        child.castShadow = true;
        child.receiveShadow = true;
      }

    });
  })

  animate()
}
// 渲染帧
function animate() {

  const deltaTime = Math.min(0.05, clock.getDelta())
  // 在控制之后,需要更新player的位置
  handleControls(deltaTime)
  updatePlayer(deltaTime)
  renderer.render(scene, camera)

  requestAnimationFrame(animate)
}
// 根据摁下键盘控制
function handleControls(deltaTime) {

  const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);

  if (keyStates['KeyW']) {
    // 摁下W,改变水平方向的速度
    player.velocity.add(getForwardVector().multiplyScalar(speedDelta));

  }

  if (keyStates['KeyS']) {

    player.velocity.add(getForwardVector().multiplyScalar(- speedDelta));

  }

  if (keyStates['KeyA']) {

    player.velocity.add(getSideVector().multiplyScalar(- speedDelta));

  }

  if (keyStates['KeyD']) {

    player.velocity.add(getSideVector().multiplyScalar(speedDelta));

  }

  if (playerOnFloor) {

    if (keyStates['Space']) {

      player.velocity.y = 15;

    }

  }
}
// 更新位置
function updatePlayer(deltaTime) {

  let damping = Math.exp(- 4 * deltaTime) - 1;

  if (!playerOnFloor) {
    player.velocity.y -= GRAVITY * deltaTime
    damping *= 0.1
  }

  player.velocity.addScaledVector(player.velocity, damping);
  // console.log(player.velocity);
  const deltaPosition = player.velocity.clone().multiplyScalar(deltaTime);

  player.geometry.translate(deltaPosition);

  playerCollision()

  camera.position.copy(player.geometry.end);

}
// 碰撞检测
function playerCollision() {
  const result = worldOctree.capsuleIntersect(player.geometry)
  playerOnFloor = false
  if (result) {
    playerOnFloor = result.normal.y > 0
    if (!playerOnFloor) {
      player.velocity.addScaledVector(result.normal, -result.normal.dot(player.velocity))
    }
    player.geometry.translate(result.normal.multiplyScalar(result.depth));
  }
}
// 获得前进方向向量
function getForwardVector() {
  camera.getWorldDirection(player.direction);
  player.direction.y = 0;
  // 转化为单位向量
  player.direction.normalize();

  return player.direction;
}
// 获得左右方向向量
function getSideVector() {
  // Camera.getWorldDirection ( target : Vector3 ) : Vector3 调用该函数的结果将赋值给该Vector3对象。
  camera.getWorldDirection(player.direction);
  player.direction.y = 0;

  // 将该向量转换为单位向量(unit vector), 也就是说,将该向量的方向设置为和原向量相同,但是其长度(length)为1。
  player.direction.normalize();
  player.direction.cross(camera.up);

  return player.direction;
}

document.addEventListener('mousedown', e => {
  document.body.requestPointerLock()
})
document.addEventListener('mousemove', e => {
  // 当鼠标在锁定状态时
  if (document.pointerLockElement === document.body) {
    camera.rotation.y -= e.movementX / 500
    camera.rotation.x -= e.movementY / 500
  }
})
document.addEventListener('keydown', e => {
  keyStates[e.code] = true
})
document.addEventListener('keyup', e => {
  keyStates[e.code] = false
})

</script>

<style>

</style>