渲染框架是基于THREE,碰撞检测是基于BVH。本来用的是three自带的octree结构做碰撞发现性能不太好
核心代码:
import * as THREE from 'three'
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
import { MeshBVH, MeshBVHHelper, StaticGeometryGenerator } from 'three-mesh-bvh';
import CameraControls from 'src/renderers/camera';
import { OrbitControls } from 'src/renderers/controls/OrbitControls'
import { Renderer } from 'src/renderers/Renderer';
class InputControls{
pressKeys=new Set()
releaseKeys=new Set()
constructor() {
this.mountEvents()
}
mountEvents(){
window.addEventListener('keydown',this.handleKey)
window.addEventListener('keyup',this.handleKey)
}
unmountEvents(){
window.removeEventListener('keydown',this.handleKey)
window.removeEventListener('keyup',this.handleKey)
}
isPressedKey(key:string){
return this.pressKeys.has(key)
}
isReleaseKey(key:string){
if(this.pressKeys.has(key)&&!this.releaseKeys.has(key)){
this.releaseKeys.add(key)
return true
}
return false
}
handleKey=(e:KeyboardEvent)=>{
const type=e.type
const key=e.key.toLowerCase()
if(type==='keydown'){
if(!this.pressKeys.has(key)){
this.pressKeys.add(key)
}
}else{
if(this.pressKeys.has(key)){
this.releaseKeys.delete(key)
this.pressKeys.delete(key)
}
}
}
}
export class CharacterPersonCamera{
keys=new Set()
player:THREE.Mesh
collider?:THREE.Mesh
colliderBox2:THREE.Box2=new THREE.Box2()
colliderBox:THREE.Box3=new THREE.Box3()
input:InputControls
speed=100
speedRatio=1 // 速率
gravity=298 // 重力速度
enableGravity=false // 是否启用重力
_enableFirstPerson=false// 是否启用第一视角
// 当前速度和位移
playerVelocity=new THREE.Vector3()
// 累积移动
accumulateMovement=new THREE.Vector3()
deltaPosition=new THREE.Vector3()
tempPlayerPosition=new THREE.Vector2()
tempVector=new THREE.Vector3()
tempVector2=new THREE.Vector3()
tempDirection=new THREE.Vector3()
tempBox=new THREE.Box3()
tempSegment=new THREE.Line3()
tempMat=new THREE.Matrix4()
playerIsOnGround=false // 是否在地面
enable=true // 是否启用
cameraControls?:CameraControls
orbitControls?:OrbitControls
upVector = new THREE.Vector3( 0, 1, 0 );
colliderBoxDistance=Infinity
constructor(public context:Renderer) {
this.input=new InputControls()
this.player=new THREE.Mesh(new RoundedBoxGeometry(0.5,1,0.5,10,1),new THREE.MeshBasicMaterial({
color:0xff0000
}))
// this.player=new THREE.Mesh(new THREE.BoxGeometry(1,1,1),generateCubeFaceTexture(512,512))
this.player.userData={
capsuleInfo:{
radius: 0.5,
segment: new THREE.Line3( new THREE.Vector3(), new THREE.Vector3( 0,0, 0.0 ) )
}
}
this.player.position.setFromMatrixPosition(this.camera.matrixWorld)
// this.root.add(this.player)
}
get renderer(){
return this.context.renderer
}
get root(){
return this.context.scene
}
get camera(){
return this.context.camera
}
get finalSpeed(){
return this.speed*this.speedRatio
}
get playerDirection(){
return this.player.quaternion
}
get isAllowFalling(){
this.tempPlayerPosition.set(this.player.position.x,this.player.position.z)
// 是否可以下落,并且当前视角位置在碰撞检测体的z轴平面上.
return this.enableGravity&&this.colliderBox2.containsPoint(this.tempPlayerPosition)
}
get minDropY(){
return this.colliderBox.min.y
}
set enableFirstPerson(v){
if(v!==this._enableFirstPerson){
this._enableFirstPerson=v;
if(!v&&this.orbitControls){
this.camera
.position
.sub( this.orbitControls.target)
.normalize()
.multiplyScalar( 10 )
.add( this.orbitControls.target);
}else if(!v&&this.cameraControls){
this.cameraControls.getTarget(this.tempVector)
this.camera
.position
.sub(this.cameraControls.getTarget(this.tempVector) )
.normalize()
.multiplyScalar( 10 )
.add(this.cameraControls.getTarget(this.tempVector));
}
}
}
get enableFirstPerson(){
return this._enableFirstPerson
}
setupOrbitControls(){
this.orbitControls=new OrbitControls(this.camera,this.renderer.domElement)
this.initControlsMaxLimit()
// this.orbitControls.enableDamping=true
// this.orbitControls.enablePan=true
// this.orbitControls.enableZoom=true
// this.orbitControls.rotateSpeed=1
// this.orbitControls.minAzimuthAngle=-Math.PI
// this.orbitControls.maxAzimuthAngle=Math.PI
}
setColliderModel(colliderModel:THREE.Object3D){
const staticGenerator = new StaticGeometryGenerator( colliderModel );
staticGenerator.attributes = [ 'position' ];
const mergedGeometry = staticGenerator.generate();
mergedGeometry.boundsTree = new MeshBVH( mergedGeometry );
this.collider = new THREE.Mesh( mergedGeometry );
mergedGeometry.boundsTree.getBoundingBox(this.colliderBox)
this.colliderBox2.min.set(this.colliderBox.min.x,this.colliderBox.min.z)
this.colliderBox2.max.set(this.colliderBox.max.x,this.colliderBox.max.z)
this.colliderBoxDistance=this.colliderBox.getSize(this.tempVector).length()*1.5
// const visualizer = new MeshBVHHelper(this.collider,1000 );
//this.root.add( visualizer );
}
updateControls(delta:number){
const finalSpeed=this.finalSpeed*delta
if(this.orbitControls){
const angle = this.orbitControls.getAzimuthalAngle();
const tempVector=this.tempVector
const upVector=this.upVector
if(this.input.isPressedKey('w')){
tempVector.set( 0, 0, - 1 ).applyAxisAngle( upVector, angle ).multiplyScalar( finalSpeed );
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('s')){
tempVector.set( 0, 0, 1 ).applyAxisAngle( upVector, angle ).multiplyScalar( finalSpeed );
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('a')){
tempVector.set( -1, 0, 0 ).applyAxisAngle( upVector, angle ).multiplyScalar( finalSpeed );
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('d')){
tempVector.set( 1, 0, 0 ).applyAxisAngle( upVector, angle ).multiplyScalar( finalSpeed );
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('q')){
tempVector.set( 0, 1, 0 ).applyAxisAngle( upVector, angle ).multiplyScalar( finalSpeed );
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('e')){
tempVector.set( 0, -1, 0 ).applyAxisAngle( upVector, angle ).multiplyScalar( finalSpeed );
this.playerVelocity.add(this.tempVector)
}
}else{
if(this.input.isPressedKey('w')){
this.tempVector.set(0,0,1).applyQuaternion(this.playerDirection).multiplyScalar(-finalSpeed)
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('s')){
this.tempVector.set(0,0,1).applyQuaternion(this.playerDirection).multiplyScalar(finalSpeed)
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('a')){
this.tempVector.set(1,0,0).applyQuaternion(this.playerDirection).multiplyScalar(-finalSpeed)
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('d')){
this.tempVector.set(1,0,0).applyQuaternion(this.playerDirection).multiplyScalar(finalSpeed)
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('q')){
this.tempVector.set(0,1,0).applyQuaternion(this.playerDirection).multiplyScalar(finalSpeed)
this.playerVelocity.add(this.tempVector)
}
if(this.input.isPressedKey('e')){
this.tempVector.set(0,1,0).applyQuaternion(this.playerDirection).multiplyScalar(-finalSpeed)
this.playerVelocity.add(this.tempVector)
}
}
}
updatePlayer(delta:number){
// 增加阻尼
const damping=0.9
if (this.enableGravity&&this.isAllowFalling&&!this.playerIsOnGround) {
this.playerVelocity.y -= delta * this.gravity;
}
this.playerVelocity.multiplyScalar(damping)
this.deltaPosition.copy(this.playerVelocity).multiplyScalar(delta)
this.accumulateMovement.add(this.deltaPosition)
// 应用移动
this.player.position.add(this.deltaPosition)
// 如果重力模式,就应用物理碰撞
if(this.enableGravity){
this.updateCollider(delta)
}
if(this.orbitControls){
// this.camera.translateZ(2)
this.camera.position.sub(this.orbitControls.target);
this.orbitControls.target.copy(this.player.position);
this.camera.position.add(this.player.position);
}else if(this.cameraControls){
this.cameraControls.getTarget(this.tempVector,true)
this.camera.position.sub(this.tempVector);
this.cameraControls.setTarget(this.player.position.x,this.player.position.y,this.player.position.z,false);
this.camera.position.add(this.player.position);
}else{
this.camera.position.copy(this.player.position)
this.camera.translateZ(2)
}
}
box3Helper?:THREE.Box3Helper
visibleBox3Helper(box:THREE.Box3){
if(!this.box3Helper){
this.box3Helper=new THREE.Box3Helper(box,0xff0000)
this.root.add(this.box3Helper)
}else{
this.box3Helper.box.copy(box)
}
}
updateCollider(delta:number){
const collider=this.collider!;
const player=this.player
const boundsTree=collider.geometry.boundsTree as MeshBVH
const tempBox=this.tempBox
const tempSegment=this.tempSegment
const tempMat=this.tempMat
const tempVector=this.tempVector
const tempVector2=this.tempVector2;
const playerVelocity=this.playerVelocity
player.updateMatrixWorld();
// 根据碰撞调整玩家位置
const capsuleInfo = player.userData.capsuleInfo;
tempBox.makeEmpty();
tempMat.copy( collider.matrixWorld ).invert();
tempSegment.copy( capsuleInfo.segment );
//获取胶囊在碰撞器局部空间中的位置
tempSegment.start.applyMatrix4( player.matrixWorld ).applyMatrix4( tempMat );
tempSegment.end.applyMatrix4( player.matrixWorld ).applyMatrix4( tempMat );
// 获取胶囊的轴对齐边界框
tempBox.expandByPoint( tempSegment.start );
tempBox.expandByPoint( tempSegment.end );
tempBox.min.addScalar( - capsuleInfo.radius );
tempBox.max.addScalar( capsuleInfo.radius );
// this.visibleBox3Helper(tempBox)
boundsTree.shapecast( {
intersectsBounds: box => box.intersectsBox( tempBox ),
intersectsTriangle: tri => {
// 检查三角形是否与胶囊相交并调整
// 胶囊位置(如果是)。
const triPoint = tempVector;
const capsulePoint =tempVector2;
const distance = tri.closestPointToSegment( tempSegment, triPoint, capsulePoint );
if ( distance < capsuleInfo.radius ) {
const depth = capsuleInfo.radius - distance;
const direction = capsulePoint.sub( triPoint ).normalize();
tempSegment.start.addScaledVector( direction, depth );
tempSegment.end.addScaledVector( direction, depth );
}
}
} );
// 检查后得到胶囊碰撞器在世界空间中的调整位置
// 三角形碰撞并移动它。 CapsuleInfo.segment.start 假设为
// 玩家模型的起源。
const newPosition = tempVector;
newPosition.copy( tempSegment.start ).applyMatrix4( collider.matrixWorld );
// 检查碰撞体移动了多少
const deltaVector = tempVector2;
deltaVector.subVectors( newPosition, player.position );
// 如果玩家主要是垂直调整的,我们假设它位于我们应该考虑地面的地方
this.playerIsOnGround = deltaVector.y > Math.abs( delta * playerVelocity.y * 0.25 );
const offset = Math.max( 0.0, deltaVector.length() - 1e-5 );
deltaVector.normalize().multiplyScalar( offset );
// 调整位置
player.position.add( deltaVector );
if ( !this.playerIsOnGround ) {
// console.log('this.playerIsOnGround',deltaVector)
deltaVector.normalize();
playerVelocity.addScaledVector( deltaVector, - deltaVector.dot( playerVelocity ) );
} else {
playerVelocity.set( 0, 0, 0 );
}
// 如果玩家跌落到水平线以下太远,则将其位置重置为开始位置
if ( player.position.y < this.minDropY ) {
this.resetPlayerPosition();
}
}
resetPlayerPosition(){
this.playerVelocity.y=0
this.player.position.y=this.minDropY
}
initControlsMaxLimit(){
const controls=this.orbitControls||this.cameraControls
if(controls){
if(this.enableFirstPerson){
controls.maxPolarAngle = Math.PI;
controls.minDistance = 1e-4;
controls.maxDistance = 1e-4;
}else{
controls.maxPolarAngle = Math.PI / 2;
controls.minDistance = 1;
controls.maxDistance = this.colliderBoxDistance
}
}
}
onUpdate(delta:number){
if(!this.enable){
return
}
this.player.quaternion.copy(this.camera.quaternion)
// this.player.quaternion.x=0
// this.player.quaternion.z=0
// this.player.quaternion.normalize()
let controls:any;
if(this.orbitControls){
controls=this.orbitControls
}
else if(this.cameraControls){
controls=this.cameraControls as any
}
this.initControlsMaxLimit()
const MAX_STEP=5;
for(let i=0;i<MAX_STEP;i++){
const d=delta/MAX_STEP;
this.updateControls(d)
this.updatePlayer(d)
}
if(controls){
controls.update(delta)
}
}
dispose(){
if(this.orbitControls){
this.orbitControls.dispose()
}
this.input.unmountEvents()
}
}