关于three.js:八叉树空间-初学

57次阅读

共计 10062 个字符,预计需要花费 26 分钟才能阅读完成。

学了 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(); // 场景对象 Scene
const 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…

正文完
 0