likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 42

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

效果:官网示例效果

第一步:初始场景的建立(一些基础的代码,没有多余的解释)

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

<script setup>
import { onMounted } from '@vue/runtime-core';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import * as THREE from 'three'

let renderer, camera, scene, container;
let clock;

onMounted(() => {
  init()
  animate()
})

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(10, 10, 10)
  camera.rotation.order = 'YXZ'

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

  // 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()

  // controls 
  const controls = new OrbitControls(camera, renderer.domElement)

  window.addEventListener("resize", onResize)

}
function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
}

// 渲染
function animate() {
  renderer.render(scene, camera)

  requestAnimationFrame(animate)
}
</script>

<style>

</style>

到这里已经可以在页面上出现3D场景 教你Vue3+Vite+threejs 创建3D场景,实现移动跳跃碰撞检测(1)

第二步:场景模型的加载

这里模型是.glb格式的,直接用GLTFLoader加载即可,如果模型被压缩过,需要使用DRACOLoader同时加载模型的配置文件

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

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

    gltf.scene.traverse(child => {

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

      }

    });
  })

  animate()
}

在 init 中调用 loadModel ,已经成功在页面上渲染出模型

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

第三步:Player的初始化

这里的Player其实就是相机的位置,相机的视角即是我们的第一视角。

  1. 先把相机位置放在原点。
  2. controls 我们先注释一下,暂时不需要controls 了
  camera.position.set(0, 0, 0)

创建player

// new Capsule(下球心(vector3),上球心(vector3),半径)
let player = {
  geometry: new Capsule(new THREE.Vector3(0, 0.35, 0), new THREE.Vector3(0, 1, 0), 0.35),
  velocity: new THREE.Vector3(),
  position: new THREE.Vector3()
}
教你Vue3+Vite+threejs 创建3D场景,实现移动跳跃碰撞检测(1)

第四步:添加键盘鼠标事件

添加鼠标点击移动事件

// 鼠标摁下,锁定鼠标
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
  }
})

这一步完成,我们的镜头已经可以随着鼠标的移动而改变方向了

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

添加键盘事件

我们用keyStates来保存键盘摁下的状态,在之后的animate函数中根据keyStates的状态来处理事件

let playerOnFloor = false
let keyStates = {}
document.addEventListener('keydown', e => {
  keyStates[e.code] = true
})
document.addEventListener('keyup', e => {
  keyStates[e.code] = false
})

第五步:根据keyStates控制状态

function handleControls(deltaTime) {
  // 如果player在地面上,速度为25
  const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);

  if (keyStates['KeyW']) {
    // 摁下W,获取当前水平向量,与这个值相乘,获得player速度改变
    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 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;
}

在animate函数中调用 handleControls() 和 updatePlayer()

function animate() {

  const deltaTime = Math.min(0.05, clock.getDelta())
  
  // 控制player移动
  handleControls(deltaTime)
  
  // 更新player的位置
  updatePlayer(deltaTime)
  renderer.render(scene, camera)

  requestAnimationFrame(animate)
}

更新player和相机的position,完成移动

function updatePlayer(deltaTime) {

  let damping = Math.exp(- 4 * deltaTime) - 1;
    
  player.velocity.addScaledVector(player.velocity, damping);
  
  // 位移距离
  const deltaPosition = player.velocity.clone().multiplyScalar(deltaTime);
  player.geometry.translate(deltaPosition);
  // 相机的位置,拷贝player的位置
  camera.position.copy(player.geometry.end);

}

此时player可以自由移动,但是还没有碰撞检测,没有跳跃

效果

先写这么多。后续再补,完整代码

<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 * as THREE from 'three'

let renderer, camera, scene, container;
let clock;
let playerOnFloor = false
let keyStates = {}

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)
  scene.add(new THREE.GridHelper(10))

  // 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)

    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 * 8;

  if (keyStates['KeyW']) {
    // 摁下W,改变水平方向的向量, multiplyScalar 乘积
    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;

  player.velocity.addScaledVector(player.velocity, damping);

  const deltaPosition = player.velocity.clone().multiplyScalar(deltaTime);
  player.geometry.translate(deltaPosition);

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

}
// 获得前进方向向量
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>
转载自:https://juejin.cn/post/7181380223349293114
评论
请登录