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

news2024/11/27 13:31:15

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1805183.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++做题

我们可以将0——9看成一个一维数组&#xff1a;a[11] #include<cstdio> int a[11],n; int x,p; int main(){scanf("%d",&n);for(int i1;i<n;i){pi;while(p!0){xp%10;a[x];//让下标x每次出现时增加1(描述不清楚)p/10;}}for(int i0;i<9;i){printf(&qu…

Linux—小小内核升级

本篇主要是讲述下关于内核的一些基本常识&#xff0c;并记录下内核升级和编译的过程&#xff0c;若有遗漏/有误之处&#xff0c;望各位大佬们指出。 Ⅰ 基本内核常识 常见内核安装包 内核(kernel)&#xff1a;这是Linux操作系统的核心部分&#xff0c;它负责管理系统的硬件和…

拉格朗日乘子将不等式约束转化为等式约束例子

拉格朗日乘子将不等式约束转化为等式约束例子 在优化问题中,常常需要将不等式约束转化为等式约束。使用拉格朗日乘子法,可以通过引入松弛变量将不等式约束转换为等式约束,然后构造拉格朗日函数进行求解。 拉格朗日乘子法简介 拉格朗日乘子法是求解带约束优化问题的一种方…

局域网测速

对于网管来说&#xff0c;企业局域网络的速度是知道的&#xff0c;因为网管清楚企业局域网络的拓扑结构、网络链路、网络设备以及实际到桌面的情况。 有时候即使千兆到桌面实际因为影响的因素多&#xff0c;实际的网络速度可能会打一定的折扣&#xff0c;那么就需要清楚实际的网…

数据挖掘分析的一点进步分享

import pandas as pd import matplotlib.pyplot as plt import numpy as npdata pd.read_csv(heros.csv,encoding"gbk") data.head() 导入数据集 进行分析 df_datadata.copy() df_data.describe()df_data.info() df_data.drop(英雄,axis1,inplaceTrue) df_data[最…

[图解]建模相关的基础知识-06

1 00:00:00,790 --> 00:00:03,480 下一个概念&#xff0c;就是基数的概念 2 00:00:04,390 --> 00:00:11,560 cardinality&#xff0c;表示有限集合中元素的数量 3 00:00:12,200 --> 00:00:14,790 我们可以用一个井号 4 00:00:14,800 --> 00:00:18,320 在前面表示…

element-plus的el-text组件(文本组件)的介绍和使用

el-text&#xff08;适合文本操作的组件&#xff09; 设置文本type,如default,primary,success,info,warning,danger超出容器尺寸自动省略&#xff0c;tuncated属性设置size属性控制文本大小&#xff0c;有large,default,small设置tag属性&#xff0c;值为html5标签名&#xf…

python - Pandas缺失值处理

文中所用数据集已上传,找不到的可以私聊我 学习目标 知道空值和缺失值的区别以及缺失值的影响 知道如何查看数据集缺失值情况的方法 知道缺失值处理的办法 1 NaN简介 好多数据集都含缺失数据。缺失数据有多种表现形式 数据库中&#xff0c;缺失数据表示为NULL 在某些编程语…

JDK下载安装Java SDK

Android中国开发者官网 Android官网 (VPN翻墙) 通过brew命令 下载OracleJDK(推荐) 手动下载OracleJDK(不推荐) oracle OracleJDK下载页 查找硬件设备是否已存在JDK环境 oracle官网 备注&#xff1a; JetPack JavaDevelopmentKit Java开发的系统SDK OpenJDK 开源免费SDK …

只需两步!使用ChatGPT搞定学术论文润色和降重(附带详细方法指令合集)

欢迎关注&#xff0c;为大家带来最酷最有效的智能AI学术科研写作攻略。关于使用ChatGPT等AI工具的相关问题可以添加作者七哥沟通 大家好&#xff0c;我将通过这篇文章分享如何借助ChatGPT提升论文的质量&#xff0c;重点是润色和降重&#xff0c;给大家分享两个顶级高效的辅助提…

关于Latitude5490的问题Bios引导问题

关于Latitude5490的问题Bios引导问题 一、问题描述1、第一次维修&#xff1a;2、第二次维修&#xff1a; 二、捣鼓过程1、Latitude 5490的Bios引导2、捣鼓硬盘分区格式3、使用PE修复引导4、处理方法 三、参考链接 一、问题描述 本人原本电脑型号为Latitude 5480&#xff0c;电…

stm32中外部中断控制Led亮灭

说明&#xff1a;外部中断的方式通过按键来实现&#xff0c;stm32的配置为江科大stm32教程中的配置。 1.内容&#xff1a; 通过中断的方式&#xff0c;按下B15按键Led亮&#xff0c;按下B13按键Led灭。 2.硬件设计&#xff1a; 3.代码&#xff1a; 3.1中断底层 EXTI.c #i…

创建 MFC DLL-使用关键字_declspec(dllexport)

本文仅供学习交流&#xff0c;严禁用于商业用途&#xff0c;如本文涉及侵权请及时联系本人将于及时删除 从MFC DLL中导出函数的另一种方法是在定义函数时使用关键字_declspec(dllexport)。这种情况下&#xff0c;不需要DEF文件。 导出函数的形式为&#xff1a; declspec(dll…

Android Ble低功耗蓝牙开发

一、新建项目 在Android Studio中新建一个项目&#xff0c;如下图所示&#xff1a; 选择No Activity&#xff0c;然后点击Next 点击Finish&#xff0c;完成项目创建。 1、配置build.gradle 在android{}闭包中添加viewBinding&#xff0c;用于获取控件 buildFeatures {viewB…

SpringBoot+Vue网上点餐系统(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 角色对应功能 用户管理员 功能截图

SpringBoot+Vue网上超市(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 角色对应功能 用户管理员 功能截图

启动游戏出现concrt140.dll错误的解决方法

concrt140.dll是一个动态链接库文件&#xff0c;属于Microsoft Visual C 2015 Redistributable组件集的一部分。这个文件是并发运行时库&#xff08;Concurrency Runtime&#xff09;的一部分&#xff0c;对于支持和增强应用程序的多线程与并发执行能力至关重要。它包含了实现并…

xshell远程无法链接上VM的centos7

1、现象如下&#xff0c; 2.解决办法&#xff1a;查证后发现这个默认的设置为vmnet0 3.参考文章&#xff1a;Xshell连接不上虚拟机centos7_centos7的nat模式可以ping通网络,但是用xshell连不上是什么原因-CSDN博客

【Python】数据处理:SQLite操作

使用 Python 与 SQLite 进行交互非常方便。SQLite 是一个轻量级的关系数据库&#xff0c;Python 标准库中包含一个名为 sqlite3 的模块&#xff0c;可以直接使用。 import sqlite3数据库连接和管理 连接到 SQLite 数据库。如果数据库文件不存在&#xff0c;则创建一个新数据库…

《精通ChatGPT:从入门到大师的Prompt指南》第9章:实战练习

第9章&#xff1a;实战练习 9.1 Prompt练习题 在本节中&#xff0c;我们将提供一系列练习题&#xff0c;旨在帮助读者通过实际操作提升使用ChatGPT的能力。这些练习题涵盖了从基础到高级的不同难度级别&#xff0c;并针对各种应用场景设计&#xff0c;确保读者能够在实际使用…