【threejs开发随笔】three.js基于八叉树的碰撞检测

0.前言

相信大家在看threejs官网案例的时候会觉得里面那个fps的小例子碰撞检测感觉很丝滑,还有一定的类似物理的效果,不像之前简单的利用射线进行碰撞检测顶到墙之后劈里啪啦卡顿。

之前一个大哥吧这个案例简单解析了一下,也介绍了一下八叉树的理论知识

three.js案例解析之游戏帧碰撞检测https://blog.csdn.net/web22050702/article/details/125301514这里就不赘述八叉树的理论和这个案例的解析了,那这儿就跟据这个案例搞一个碰撞检测的控制器CollisionController,方便大家直接用。下面的连接是本文的完整代码,需要js版本请复制代码到ts官网playground中根据自己需要的js版本自行编译。

octreeCollision.ts https://gitee.com/Susiia/threejs-standard-framework/blob/master/src/utils/octreeCollision.ts


1.先说用法

先引入

import { CollisionController } from './octreeCollision'

 然后使用,格式如下

new CollisionController(capsuleParam: iCapsule, camera: PerspectiveCamera, collisionGroup: Object3D<Event>, canvas: HTMLCanvasElement): CollisionController

说人话就是new 一个CollisionController实例,传入相机体积参数(同Capsule)、需要控制的相机(PerspectiveCamera)、需要进行碰撞检测的模型(Object3D)、渲染画布(canvas)

    // 控制器
    private controls () {
      this.controls = new CollisionController(
        {
          start: new Vector3(0, 0.35, 0),
          end: new Vector3(0, 1, 0),
          radius: 0.35
        },
        this.camera,
        this.scene.children[this.scene.children.length - 1],
        this.renderer.domElement)
      this.controls.update()
    }

然后在渲染循环中对这个控制器进行更新

    // 渲染循环
    private renderLoop () {
      this.renderer.render(this.scene, this.camera)
      this.controls?.update()
    }

2.再说内容

如何创建一个threejs项目这里就不赘述,我这儿有个vue3+ts+threejs的项目模板,有需要的话可以fork一下,里面后续也会更新一些实用的小工具

threejs-standard-framework: vue框架threejs标准模板 (gitee.com)https://gitee.com/Susiia/threejs-standard-framework新建octreeCollision.ts,首先我们需要引入的东西有

import { Clock, Object3D, PerspectiveCamera, Vector3 } from 'three'
import { Octree } from 'three/examples/jsm/math/Octree.js'
import { Capsule } from 'three/examples/jsm/math/Capsule.js'

创建构造函数需要的接口(碰撞体积参数类型)

interface iCapsule{
    start:Vector3,
    end:Vector3,
    radius:number
}

创建CollisionController类

class CollisionController {
    constructor (capsuleParam:iCapsule, camera:PerspectiveCamera, collisionGroup:Object3D, canvas:HTMLCanvasElement) {
    }
}

声明私有变量

    private clock:Clock // 时钟
    private camera:PerspectiveCamera // 相机
    private collisionGroup:Object3D// 需要计算碰撞检测的组
    private canvas:HTMLCanvasElement
    private GRAVITY:number // 重力
    private STEPS_PER_FRAME:number // 每秒步数
    private worldOctree:Octree// 环境八叉树
    private _playerCollider!: Capsule // 玩家碰撞体积(公开方法,setter和getter在下面)
    private playerOnFloor:boolean;// 玩家是不是在地面上
    private playerVelocity:Vector3;// 玩家速度
    private playerDirection:Vector3;// 玩家方向
    private eventStates = { // 事件状态
      KeyW: false,
      KeyA: false,
      KeyS: false,
      KeyD: false,
      Space: false,
      mouseDown: false
    }

在构造函数中对声明的变量进行初始化

    constructor (capsuleParam:iCapsule, camera:PerspectiveCamera, collisionGroup:Object3D, canvas:HTMLCanvasElement) {
      this.clock = new Clock()
      this.GRAVITY = 30
      this.STEPS_PER_FRAME = 5
      this.worldOctree = new Octree()
      this.playerOnFloor = false
      this.playerCollider = new Capsule(capsuleParam.start, capsuleParam.end, capsuleParam.radius)
      this.playerVelocity = new Vector3()
      this.playerDirection = new Vector3()
      this.camera = camera
      this.camera.rotation.order = 'YXZ' // 相机旋转方式需要调整一下
      this.collisionGroup = collisionGroup
      this.canvas = canvas
      // 将需要进行碰撞检测的Object3D加入worldOctree
      this.worldOctree.fromGraphNode(this.collisionGroup) 
      this.initEventListener() // 开启事件侦听
    }

事件侦听方法:

// 初始化按键侦听
    private initEventListener () {
      // 键盘按下
      document.addEventListener('keydown', (event) => {
        switch (event.code) {
          case 'KeyW':
            this.eventStates[event.code] = true
            break
          case 'KeyA':
            this.eventStates[event.code] = true
            break
          case 'KeyS':
            this.eventStates[event.code] = true
            break
          case 'KeyD':
            this.eventStates[event.code] = true
            break
          case 'Space':
            this.eventStates[event.code] = true
            break
          default:
            break
        }
      })
      // 键盘抬起
      document.addEventListener('keyup', (event) => {
        switch (event.code) {
          case 'KeyW':
            this.eventStates[event.code] = false
            break
          case 'KeyA':
            this.eventStates[event.code] = false
            break
          case 'KeyS':
            this.eventStates[event.code] = false
            break
          case 'KeyD':
            this.eventStates[event.code] = false
            break
          case 'Space':
            this.eventStates[event.code] = false
            break
          default:
            break
        }
      })
      // 鼠标按下
      this.canvas.addEventListener('mousedown', () => {
        // this.canvas.requestPointerLock()
        this.eventStates.mouseDown = true
      })
      // 鼠标抬起
      this.canvas.addEventListener('mouseup', () => {
        // NOTE:exitPointerLock在ts中会报错, ts3.1一些浏览器厂商特定的类型从lib.d.ts中被移除,其中包括退出PointerLock状态的方法exitPointerLock(),https://www.lanqiao.cn/library/TypeScript/breaking-changes/typescript-3.1
        // this.canvas.exitPointerLock()
        this.eventStates.mouseDown = false
      })
      // 鼠标移动
      this.canvas.addEventListener('mousemove', (event) => {
        if (this.eventStates.mouseDown) {
          this.camera.rotation.y -= event.movementX / 500
          this.camera.rotation.x -= event.movementY / 500
        }
      })
    }

创建一个控制器方法

   // 控制器
    private controls (deltaTime:number) {
      // gives a bit of air control
      const speedDelta = deltaTime * (this.playerOnFloor ? 25 : 8)

      if (this.eventStates.KeyW) {
        this.playerVelocity.add(this.getForwardVector().multiplyScalar(speedDelta))
      }

      if (this.eventStates.KeyS) {
        this.playerVelocity.add(this.getForwardVector().multiplyScalar(-speedDelta))
      }

      if (this.eventStates.KeyA) {
        this.playerVelocity.add(this.getSideVector().multiplyScalar(-speedDelta))
      }

      if (this.eventStates.KeyD) {
        this.playerVelocity.add(this.getSideVector().multiplyScalar(speedDelta))
      }

      if (this.playerOnFloor) {
        if (this.eventStates.Space) {
          this.playerVelocity.y = 15
        }
      }
    }

创建一个public方法出去,用来更新控制器

    // 更新控制器
    public update () {
      const deltaTime = Math.min(0.05, this.clock.getDelta()) / this.STEPS_PER_FRAME
      for (let i = 0; i < this.STEPS_PER_FRAME; i++) {
        this.controls(deltaTime)
        this.updatePlayer(deltaTime)
      }
    }

创建一个用来更新玩家的方法

    // 更新玩家
    private updatePlayer (deltaTime:number) {
      let damping = Math.exp(-4 * deltaTime) - 1
      if (!this.playerOnFloor) {
        this.playerVelocity.y -= this.GRAVITY * deltaTime
        damping *= 0.1
      }
      this.playerVelocity.addScaledVector(this.playerVelocity, damping)
      const deltaPosition = this.playerVelocity.clone().multiplyScalar(deltaTime)
      this.playerCollider.translate(deltaPosition)
      this.playerCollisions()
      this.camera.position.copy(this.playerCollider.end)
    }

创建获得当前相机前后和左右方向的方法
 

    // 获得前后方向
    private getForwardVector () {
      this.camera.getWorldDirection(this.playerDirection)
      this.playerDirection.y = 0
      this.playerDirection.normalize()

      return this.playerDirection
    }

    // 获得左右方向
    private getSideVector () {
      this.camera.getWorldDirection(this.playerDirection)
      this.playerDirection.y = 0
      this.playerDirection.normalize()
      this.playerDirection.cross(this.camera.up)

      return this.playerDirection
    }

创建碰撞检测方法

    // 玩家碰撞
    private playerCollisions () {
      const result = this.worldOctree.capsuleIntersect(this.playerCollider)
      this.playerOnFloor = false
      if (result) {
        this.playerOnFloor = result.normal.y > 0
        if (!this.playerOnFloor) {
          this.playerVelocity.addScaledVector(result.normal, -result.normal.dot(this.playerVelocity))
        }
        this.playerCollider.translate(result.normal.multiplyScalar(result.depth))
      }
    }

到此我们这个控制器就写好了。

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值