学了 three 元宇宙开发后发现人物只能在立体上行走,于是我人物上楼梯 跳跃就特地的好奇……
对于常识的好奇不下于我爱喝啤酒,于是我全世界的去找材料和在技术群里问同学,终于让我晓得three 有一个货色叫 八叉树 接下来记录一下我用 八叉树 的一些小心得……

必须的引入的库

import { Octree } from 'three/examples/jsm/math/Octree.js';import { Capsule } from 'three/examples/jsm/math/Capsule.js';

这两个库必须引入,在 八叉树空间 的所有撞检和重力都靠这两个库实现

申明 Capsule 和 Octree

const worldOctree = new Octree();const playerCollider = new Capsule( new Vector3(0,0.35,0), new Vector3(0,1,0), 0.35 );

场景模型引入增加入八叉树空间

new GLTFLoader().setPath("./model/test/").load("collision-world.glb", (gltf) => {  ...  worldOctree.fromGraphNode( gltf.scene );  // 把场景模型退出到八叉树空间  ...});

整体的来说就是 整个八叉树空间 就是 Capsule 和 Octree 运算

全副代码

<!--  --><template>  <div id="three" ref="three">    <div ref="le" @mousedown="ddmousedown"></div>  </div></template><script setup lang="ts">import { ref, reactive, onMounted, onUnmounted } from "vue";import * as dat from "dat.gui";import {  ACESFilmicToneMapping,  AmbientLight,  AnimationAction,  AnimationMixer,  AnimationUtils,  AxesHelper,  BackSide,  CameraHelper,  Clock,  Color,  DirectionalLight,  DirectionalLightHelper,  Fog,  Group,  HemisphereLight,  IcosahedronGeometry,  Mesh,  MeshBasicMaterial,  MeshLambertMaterial,  PerspectiveCamera,  PlaneGeometry,  PointLight,  Raycaster,  Scene,  Sphere,  SpotLight,  sRGBEncoding,  TextureLoader,  Vector3,  VideoTexture,  VSMShadowMap,  WebGLRenderer,} from "three";import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";import Stats from 'three/examples/jsm/libs/stats.module.js';import { Octree } from 'three/examples/jsm/math/Octree.js';import { OctreeHelper } from 'three/examples/jsm/helpers/OctreeHelper.js';import { Capsule } from 'three/examples/jsm/math/Capsule.js';/** * 配置 */// MARK: 配置=======const le = ref();const three = ref();const donuts = ref();const scene = new Scene(); // 场景对象Sceneconst axes = new AxesHelper(50); // 轴长度const th = reactive({  ctrl: new dat.GUI({ width: 200 }),  renderer: new WebGLRenderer({    antialias: true,  }), // 渲染器对象  ambienLight: new AmbientLight(0xffffff, 0.1), // 自然光  planeGeometry: new PlaneGeometry(100, 100), // 高空  spotLight: new SpotLight(0xffffff), // 聚光灯  pointlight: new PointLight(0xffffff, 6, 60), // 点光源  directionalLight: new DirectionalLight(0xffffff, 0.2), // 平行光源  hemisphereLight: new HemisphereLight(0xffffff, 0x00ff00, 1), // 半球光光源});scene.background = new Color(0x88ccee);scene.fog = new Fog( 0x88ccee, 0, 50 );// scene.add(axes); // 增加轴const clock =ref<any>(new Clock());const stats = ref<any>(Stats());let helper:any = null;const GRAVITY = ref<number>(30);const NUM_SPHERES = ref<number>(100);const SPHERE_RADIUS = ref<number>(0.2);const STEPS_PER_FRAME = ref<number>(5);const worldOctree = new Octree();const playerCollider = new Capsule( new Vector3(0,0.35,0), new Vector3(0,1,0), 0.35 );const playerVelocity = new Vector3();const playerDirection = new Vector3();let playerOnFloor = false;let mouseTime = 0;const keyStates:any = {};/** * 影相机 */// MARK: 影相机=======let camera = new PerspectiveCamera(75,window.innerWidth / window.innerHeight,0.1,1000); //透视相机camera.rotation.order = 'YXZ';/** * 灯光 */// MARK: 灯光=======scene.add(th.ambienLight); // 自然光th.directionalLight.position.set( - 5, 25, - 1 );th.directionalLight.castShadow = true;th.directionalLight.shadow.camera.near = 0.01;th.directionalLight.shadow.camera.far = 500;th.directionalLight.shadow.camera.right = 30;th.directionalLight.shadow.camera.left = - 30;th.directionalLight.shadow.camera.top    = 30;th.directionalLight.shadow.camera.bottom = - 30;th.directionalLight.shadow.mapSize.width = 1024;th.directionalLight.shadow.mapSize.height = 1024;th.directionalLight.shadow.radius = 4;th.directionalLight.shadow.bias = - 0.00006;scene.add( th.directionalLight );// const helper = new DirectionalLightHelper(th.directionalLight, 5);// scene.add(helper);const dirCameraHelper = new CameraHelper(th.directionalLight.shadow.camera);// scene.add(dirCameraHelper);const hemisphereLight = new HemisphereLight( 0x4488bb, 0x002244, 0.5 );hemisphereLight.position.set( 2, 1, 1 );scene.add( hemisphereLight );/** * 渲染 场景 */// MARK: 渲染 场景=======th.renderer.shadowMap.enabled = true;th.renderer.setPixelRatio( window.devicePixelRatio );th.renderer.setSize( window.innerWidth, window.innerHeight );th.renderer.shadowMap.enabled = true;th.renderer.shadowMap.type = VSMShadowMap;th.renderer.outputEncoding = sRGBEncoding;th.renderer.toneMapping = ACESFilmicToneMapping;/** * Stats 性能检测 */// MARK: 性能检测=======stats.value.domElement.style.position = 'absolute';stats.value.domElement.style.top = '0px';/** * 高空 */// MARK: 高空=======/** * OrbitControls 轨道控制器 */// MARK: OrbitControls 轨道控制器=======/** * 导入模型 */// MARK: 导入模型=======let playerMesh:any= null;let playerMixer= ref<AnimationMixer>();let actionWalk= ref<AnimationAction>();let actionIdle= ref<AnimationAction>();const lookTarget = new Vector3(0, 2, 0);new GLTFLoader().setPath("./model/test/").load("player2.glb", gltf => {  // 角色  playerMesh = gltf.scene;  gltf.scene.position.set(0,0,0);  gltf.scene.rotateY(Math.PI);  gltf.scene.traverse((child)=>{    child.receiveShadow = true;    child.castShadow = true;  })  camera.position.set(0, 2.5, -5);  // camera.lookAt(playMesh.value.position);  camera.lookAt(lookTarget);  const pointLight: PointLight = new PointLight(0xffffff, 1);  pointLight.position.set(0, 2, -1);  playerMixer.value = new AnimationMixer(gltf.scene);  const clipWalk = AnimationUtils.subclip(gltf.animations[0], 'walk', 0, 30);  actionWalk.value = playerMixer.value.clipAction(clipWalk);  const clipIdle = AnimationUtils.subclip(gltf.animations[0], 'idle', 31, 281);  actionIdle.value = playerMixer.value.clipAction(clipIdle);  actionIdle.value.play();  gltf.scene.add(pointLight);  gltf.scene.add(camera);  scene.add(gltf.scene);});new GLTFLoader().setPath("./model/test/").load("collision-world.glb", (gltf) => {  // console.log(gltf);  scene.add(gltf.scene);  donuts.value = gltf.scene;  worldOctree.fromGraphNode( gltf.scene );  gltf.scene.traverse( (child:any) => {    if ( child.isMesh ) {      child.castShadow = true;      child.receiveShadow = true;      if ( child.material.map ) {        child.material.map.anisotropy = 4;      }    }  } );  helper = new OctreeHelper( worldOctree );  helper.visible = false;  scene.add( helper );  animate();});/** * 建盘管制人物 */// MARK: 建盘管制人物=======let isWalk = ref<boolean>(false);const playerHalfHeight = new Vector3(0,0.3, 0);window.addEventListener("keydown", event => {  keyStates[event.code] = true;  if (event.key == "w") {    if (!isWalk.value) {      if( actionIdle.value && actionWalk.value){        crossPlay(actionIdle.value,actionWalk.value);      }      isWalk.value = true;    }  }});window.addEventListener("keyup", event => {  keyStates[ event.code ] = false;  if (event.key == "w") {    if( actionIdle.value && actionWalk.value){      crossPlay(actionWalk.value,actionIdle.value);    }    isWalk.value = false;  }});let ddmousedown =()=>{  document.body.requestPointerLock();  mouseTime = performance.now();}document.addEventListener( 'mouseup', () => {  // if ( document.pointerLockElement !== null ) throwBall();});let preClientX:number;window.addEventListener("mousemove", event => {  if(document.pointerLockElement === document.body){    playerMesh.rotation.y -= -(event.movementX / 300);    // playerMesh.rotation.x -= -(event.movementY / 2000);  }});let playerCollisions = ()=> {  const result = worldOctree.capsuleIntersect( playerCollider );  playerOnFloor = false;  if ( result ) {    playerOnFloor = result.normal.y > 0;    if ( ! playerOnFloor ) {      playerVelocity.addScaledVector( result.normal, - result.normal.dot( playerVelocity ) );    }    playerCollider.translate( result.normal.multiplyScalar( result.depth ) );  }}let updatePlayer = ( deltaTime:number )=> {  let damping = Math.exp(-4*deltaTime)-1;  if ( ! playerOnFloor ) {    playerVelocity.y-=GRAVITY.value*deltaTime;    // small air resistance    damping *= 0.1;  }  playerVelocity.addScaledVector( playerVelocity, damping );  const deltaPosition = playerVelocity.clone().multiplyScalar( deltaTime );    playerCollider.translate( deltaPosition );  playerCollisions();  if(playerMesh) playerMesh.position.copy( playerCollider.end );}let getForwardVector = ()=>{  playerMesh.getWorldDirection( playerDirection );  playerDirection.y = 0;  playerDirection.normalize();  return playerDirection;}let getSideVector = ()=>{  playerMesh.getWorldDirection( playerDirection );  playerDirection.y = 0;  playerDirection.normalize();  playerDirection.cross( playerMesh.up );  return playerDirection;}let controls=( deltaTime:number )=>{  // gives a bit of air control  const speedDelta = deltaTime * ( playerOnFloor ? 25 : 8 );  if ( keyStates[ 'KeyW' ] ) {    playerVelocity.add( getForwardVector().multiplyScalar(speedDelta*0.6));  }  if ( keyStates[ 'KeyS' ] ) {    // playerVelocity.add( getForwardVector().multiplyScalar(-speedDelta));  }  if ( keyStates[ 'KeyA' ] ) {    // playerVelocity.add( getSideVector().multiplyScalar(-speedDelta));  }  if ( keyStates[ 'KeyD' ] ) {    // playerVelocity.add( getSideVector().multiplyScalar(speedDelta));  }  if ( playerOnFloor ) {    if ( keyStates[ 'Space' ] ) {      playerVelocity.y = 15;    }  }}let teleportPlayerIfOob=()=>{  if(!playerMesh) return;  if ( playerMesh.position.y <= - 25 ) {    playerCollider.start.set( 0, 0.35, 0 );    playerCollider.end.set( 0, 1, 0 );    playerCollider.radius = 0.35;    playerMesh.position.copy( playerCollider.end );    playerMesh.rotation.set( 0, 0, 0 );  }}let crossPlay = (curAction:AnimationAction, newAction:AnimationAction)=>{  curAction.fadeOut(0.3);  newAction.reset();  newAction.setEffectiveWeight(1);  newAction.play();  newAction.fadeIn(0.3);}let render = () => {  th.renderer.render(scene, camera);};//============================================场景搭建end==================================onMounted(() => {  le.value.appendChild(th.renderer.domElement);  le.value.appendChild(stats.value.domElement);  window.addEventListener(    "resize",    () => {      camera.aspect = window.innerWidth / window.innerHeight;      camera.updateProjectionMatrix();      th.renderer.setSize(window.innerWidth, window.innerHeight);      render();    },    false  );});//============================================构建场景 start==================================// MARK: 构建场景=======//============================================构建场景 end==================================//============================================gui start==================================/** * dat.gui 插件 */// MARK: dat.gui 插件=======let ctrlObj2 = {};let basicfolder = th.ctrl.addFolder("MeshBasicMaterial");basicfolder.add( { debug: false }, 'debug' ).onChange(value=>{  helper.visible = value;});basicfolder.open();//============================================gui end==================================let animate=()=> {  const deltaTime = Math.min(0.05, clock.value.getDelta())/STEPS_PER_FRAME.value;  //咱们在子步骤中寻找碰撞,以升高危险  //一个物体疾速穿过另一个物体而无奈检测。  for ( let i = 0; i < STEPS_PER_FRAME.value; i ++ ) {    controls( deltaTime );    updatePlayer( deltaTime );    teleportPlayerIfOob();  }  th.renderer.render( scene, camera );  stats.value.update();  if (playerMixer.value) {    playerMixer.value.update(0.045);  }  requestAnimationFrame( animate );}onUnmounted(() => {  th.ctrl.destroy(); // 销毁dat.GUI});</script><style scoped lang="less">@import "~@/common/less/index.less";</style>

运行成果如下

https://www.bilibili.com/vide...