学了 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…