简单的基于threejs和BVH第一人称视角和第三人称视角控制器

渲染框架是基于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()
    }
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值