简单的基于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()
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【iOS】JSONModel源码阅读笔记

文章目录 前言一、JSONModel使用二、JSONModel其他方法转换属性名称 三、源码分析- (instancetype)initWithDictionary:(NSDictionary*)dict error:(NSError **)err[self init]__setup____inspectProperties - (BOOL)__doesDictionary:(NSDictionary*)dict matchModelWithKeyMa…

小白都可以通过U盘重装系统,再也不用花50块钱去安装系统啦

下载Ventoy 软件 1、今天带着大家通过Ventoy 安装Windows 11 系统。 2、首先我们通过官网如下地址&#xff1a;https://www.ventoy.net/cn/&#xff0c;找到我们对应系统的Ventoy 软件安装包。 3、通过官网可以找到软件包的地址地址&#xff0c;如下图所示。 4、如下就是我下…

面试题 17.15. 最长单词

. - 力扣&#xff08;LeetCode&#xff09; class Solution { public: struct Trie {Trie() {end false;next.resize(26, nullptr);}bool end;std::vector<Trie*> next; };void insert_trie(Trie* root, std::string& word) {Trie* cur root;for (int i 0; i <…

《MySQL是怎样运行的》读书笔记(三) B+树索引

前言 从前面数据存储结构中我们已经知道了页和记录的关系示意图: 其中页a、页b、页c ... 页n 这些页可以不在物理结构上相连&#xff0c;只要通过双向链表相关联即可。 在正式介绍索引之前&#xff0c;我们需要了解一下没有索引的时候是怎么查找记录的。下边先只讨论搜索条件…

SpringBoot 参数验证的几种方式

文章目录 SpringBoot 参数验证1、为什么要进行参数验证2、验证方式2.1 if 语句判断2.2 Assert2.3 Validator2.3.1 引入依赖2.3.2 定义参数实体类2.3.4 定义特定异常全局拦截方法2.3.5 定义校验类进行测试2.3.6 测试 2.4 自定义验证注解2.4.1 定义自定义注解2.4.2 定义自定义验证…

C#操作MySQL从入门到精通(20)——更新数据

前言: 谈到数据库,大家最容易脱口而出的就是增删改查,本文所说的更新数据就是增删改查的改,改变数据的意思。 本文测试使用的数据库如下: 1、更新一列 所谓更新一列的意思就是只更改一列数据,并且通常要使用where条件,因为不加这个条件的话会导致将所有行的数据进行…

Java | Leetcode Java题解之第137题只出现一次的数字II

题目&#xff1a; 题解&#xff1a; class Solution {public int singleNumber(int[] nums) {int a 0, b 0;for (int num : nums) {b ~a & (b ^ num);a ~b & (a ^ num);}return b;} }

十大人工智能企业

​​​​​​链接&#xff1a;​​​​​​人工智能品牌排行-人工智能十大公司-人工智能十大品牌-Maigoo品牌榜

Linux--进程间通信(system V共享内存)

目录 1.原理部分 2.系统调用接口 参数说明 返回值 1. 函数原型 2. 参数说明 3. 返回值 4. 原理 5. 注意事项 3.使用一下shmget&#xff08;一段代码&#xff09; 4.一个案例&#xff08;一段代码) 1.简单封装一下 2.使用共享内存 2.1挂接&#xff08;shmat&#x…

2024 年适用于 Linux 的 5 个微软 Word 替代品

对于那些最近由于隐私问题或其他原因而转向 Linux 的用户来说&#xff0c;可能很难替换他们最喜欢的、不在 Linux 操作系统上运行的应用程序。 寻找流行程序的合适替代品可能会成为一项挑战&#xff0c;而且并不是每个人都准备好花费大量时间来尝试弄清楚什么可以与他们在 Win…

新买的移动硬盘无法识别

文章目录 背景解决方案 背景 同事新买的移动硬盘&#xff0c;插在电脑上识别不出来盘符&#xff0c;检查了一下&#xff0c;硬盘没问题应该&#xff0c;是ssk的硬盘盒M.2的SSD&#xff0c;硬盘驱动也是正常的&#xff0c;插拔了几次&#xff0c;都不识别&#xff0c;换了太电脑…

fl studio怎么设置中文及 2024年最新fl studio选购指南

FL Studio让你的计算机就像是全功能的录音室&#xff0c;漂亮的大混音盘&#xff0c;先进的创作工具&#xff0c;让你的音乐突破想象力的限制。zol提供FL Studio中文版下载。 FL Studio中文版下载软件简介 FL Studio 让你的计算机就像是全功能的录音室&#xff0c;漂亮的大混…

基于实验的电动汽车动力电池SOC

前言 本文为笔者在学习《基于MATLAB的新能源汽车仿真》过程中学习笔记&#xff0c;所涉及的表格数据和公式均为书籍里的。仿真数据是网上找的恒电流放电数据。本文仅作为笔者的入门学习记录。 一、分析动力电池SOC估算方法 SOC是指动力电池按照规定放电条件可以释放的容量占…

java版知识付费saas租户平台:剖析现代知识付费平台的功能架构与运营逻辑

在数字化学习的时代背景下&#xff0c;知识付费平台已经成为教育行业的一颗璀璨明星&#xff0c;以其用户需求为中心&#xff0c;提供便捷高效的学习途径。这些平台汇聚了众多专业知识&#xff0c;覆盖职业技能、生活兴趣和人文社科等多个领域&#xff0c;满足不同用户的学习需…

安全专业的硬件远控方案 设备无网也能远程运维

在很多行业中&#xff0c;企业的运维工作不仅仅局限在可以联网的IT设备&#xff0c;不能连接外网的特种设备也需要专业的远程运维手段。 这种特种设备在能源、医疗等行业尤其常见&#xff0c;那么我们究竟如何通过远程控制&#xff0c;对这些无网设备实施远程运维&#xff0c;…

[Algorithm][动态规划][01背包问题][目标和][最后一块石头的重量Ⅱ]详细讲解

目录 1.目标和1.题目链接2.算法原理详解3.代码实现 2.最后一块石头的重量 II1.题目链接2.算法原理详解3.代码实现 1.目标和 1.题目链接 目标和 2.算法原理详解 问题转化&#xff1a;在数组中选择一些数&#xff0c;让这些数的和等于a&#xff0c;一共有多少种选法&#xff1f…

推荐4个好用有趣的软件

MyComic——漫画聚合软件 MyComic是一款界面简洁、分类详尽的漫画阅读软件&#xff0c;专为动漫爱好者设计。它提供了丰富的高清漫画资源&#xff0c;支持在线免费阅读&#xff0c;并且可以一键下载到书架&#xff0c;方便随时离线观看&#xff0c;节省流量。用户可以轻松找到喜…

CSAPP Lab01——Data Lab完成思路

陪你把想念的酸拥抱成温暖 陪你把彷徨写出情节来 未来多漫长再漫长还有期待 陪伴你 一直到 故事给说完 ——陪你度过漫长岁月 完整代码见&#xff1a;CSAPP/datalab-handout at main SnowLegend-star/CSAPP (github.com) 01 bitXor 这道题是用~和&计算x^y。 异或是两个…

Android Compose 十:常用组件列表 监听

1 去掉超出滑动区域时的拖拽的阴影 即 overScrollMode 代码如下 CompositionLocalProvider(LocalOverscrollConfiguration provides null) {LazyColumn() {items(list, key {list.indexOf(it)}){Row(Modifier.animateItemPlacement(tween(durationMillis 250))) {Text(text…

接口(API)开发,测试工具-apifox

前言 为什么需要接口&#xff08;API&#xff09;? 因为不同的平台或系统可能使用不同的技术栈、编程语言或数据格式。API提供了一个标准化的方式&#xff0c;使得这些不同的系统可以相互交换数据和功能调用&#xff0c;实现互操作性 在开发日常的项目交互中&#xff0c;不…