From f21b386fbe4c124e05c54a12149989f3d7415974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 14 Oct 2024 14:05:23 +0200 Subject: [PATCH 01/16] feat: base collisions for spheres --- src/lib/physics/voxelmap-collisions.ts | 95 ++++++++++++++++++++++++++ src/test/test-physics.ts | 58 ++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 812c5ab3..20d27eab 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -11,6 +11,20 @@ type RayIntersection = { readonly point: THREE.Vector3Like; }; +type SphereIntersection = { + readonly normal: THREE.Vector3; + readonly depth: number; +}; + +function clamp(x: number, min: number, max: number): number { + if (x < min) { + return min; + } else if (x > max) { + return max; + } + return x; +} + class VoxelmapCollisions { private readonly voxelmapCollider: VoxelmapCollider; @@ -83,6 +97,87 @@ class VoxelmapCollisions { return null; } + + public sphereIntersect(sphere: THREE.Sphere): SphereIntersection | null { + const displacements: THREE.Vector3Like[] = []; + const addDisplacementIfNeeded = (sphereCenterProjection: THREE.Vector3Like) => { + const projectionToCenter = new THREE.Vector3().subVectors(sphere.center, sphereCenterProjection); + const distanceFromCenter = projectionToCenter.length(); + if (distanceFromCenter < sphere.radius) { + displacements.push(projectionToCenter.normalize().multiplyScalar(sphere.radius - distanceFromCenter)); + } + }; + + const voxelFrom = sphere.center.clone().subScalar(sphere.radius).floor(); + const voxelTo = sphere.center.clone().addScalar(sphere.radius).floor(); + + const voxel = { x: 0, y: 0, z: 0 }; + for (voxel.z = voxelFrom.z; voxel.z <= voxelTo.z; voxel.z++) { + for (voxel.y = voxelFrom.y; voxel.y <= voxelTo.y; voxel.y++) { + for (voxel.x = voxelFrom.x; voxel.x <= voxelTo.x; voxel.x++) { + const localSphereCenter: THREE.Vector3Like = new THREE.Vector3().subVectors(sphere.center, voxel); + const sphereCenterLocalProjection2dX = this.pointSquareProjection({ x: localSphereCenter.z, y: localSphereCenter.y }); + const sphereCenterLocalProjection2dY = this.pointSquareProjection({ x: localSphereCenter.x, y: localSphereCenter.z }); + const sphereCenterLocalProjection2dZ = this.pointSquareProjection({ x: localSphereCenter.x, y: localSphereCenter.y }); + + const sphereCenterProjection2dX = { x: sphereCenterLocalProjection2dX.x + voxel.z, y: sphereCenterLocalProjection2dX.y + voxel.y }; + const sphereCenterProjection2dY = { x: sphereCenterLocalProjection2dY.x + voxel.x, y: sphereCenterLocalProjection2dY.y + voxel.z }; + const sphereCenterProjection2dZ = { x: sphereCenterLocalProjection2dZ.x + voxel.x, y: sphereCenterLocalProjection2dZ.y + voxel.y }; + + const voxelIsFull = this.voxelmapCollider.getVoxel(voxel) !== EVoxelStatus.EMPTY; + + const voxelBelowIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y - 1, z: voxel.z }) !== EVoxelStatus.EMPTY; + const voxelAboveIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y + 1, z: voxel.z }) !== EVoxelStatus.EMPTY; + const voxelLeftIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x - 1, y: voxel.y, z: voxel.z }) !== EVoxelStatus.EMPTY; + const voxelRightIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x + 1, y: voxel.y, z: voxel.z }) !== EVoxelStatus.EMPTY; + const voxelBackIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y, z: voxel.z - 1 }) !== EVoxelStatus.EMPTY; + const voxelFrontIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y, z: voxel.z + 1 }) !== EVoxelStatus.EMPTY; + + if (voxelIsFull !== voxelBelowIsFull) { + addDisplacementIfNeeded({ x: sphereCenterProjection2dY.x, y: voxel.y, z: sphereCenterProjection2dY.y }); + } + if (voxelIsFull !== voxelAboveIsFull) { + addDisplacementIfNeeded({ x: sphereCenterProjection2dY.x, y: voxel.y + 1, z: sphereCenterProjection2dY.y }); + } + if (voxelIsFull !== voxelLeftIsFull) { + addDisplacementIfNeeded({ x: voxel.x, y: sphereCenterProjection2dX.y, z: sphereCenterProjection2dX.x }); + } + if (voxelIsFull !== voxelRightIsFull) { + addDisplacementIfNeeded({ x: voxel.x + 1, y: sphereCenterProjection2dX.y, z: sphereCenterProjection2dX.x }); + } + if (voxelIsFull !== voxelBackIsFull) { + addDisplacementIfNeeded({ x: sphereCenterProjection2dZ.x, y: sphereCenterProjection2dZ.y, z: voxel.z }); + } + if (voxelIsFull !== voxelFrontIsFull) { + addDisplacementIfNeeded({ x: sphereCenterProjection2dZ.x, y: sphereCenterProjection2dZ.y, z: voxel.z + 1 }); + } + } + } + } + + if (displacements.length > 0) { + const totalDisplacement = new THREE.Vector3(); + for (const displacement of displacements) { + totalDisplacement.add(displacement); + } + totalDisplacement.divideScalar(displacements.length); + const totalDepth = totalDisplacement.length(); + return { + normal: totalDisplacement.normalize(), + depth: totalDepth, + }; + } + + return null; + } + + /* Computes the projection of a point onto the {0,1}² square. */ + private pointSquareProjection(point: THREE.Vector2Like): THREE.Vector2 { + return new THREE.Vector2( + clamp(point.x, 0, 1), + clamp(point.y, 0, 1), + ); + } } export { VoxelmapCollisions }; diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index 171d660c..d1958a8a 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -13,6 +13,12 @@ import { import { TestBase } from './test-base'; +type SolidSphere = { + readonly mesh: THREE.Mesh; + readonly collider: THREE.Sphere; + readonly velocity: THREE.Vector3; +}; + class TestPhysics extends TestBase { private readonly map: IVoxelMap; @@ -29,6 +35,10 @@ class TestPhysics extends TestBase { readonly intersectionMesh: THREE.Mesh; }; + private readonly spheres: SolidSphere[] = []; + + private lastUpdate: number | null = null; + public constructor(map: IVoxelMap) { super(); @@ -108,9 +118,32 @@ class TestPhysics extends TestBase { this.scene.add(rayControls); this.setRayLength(10); + + const sphereRadius = 1.1; + const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(sphereRadius), new THREE.MeshPhongMaterial({ color: 0xdddddd })); + + window.addEventListener("keyup", event => { + if (event.code === "Space") { + const direction = this.camera.getWorldDirection(new THREE.Vector3()); + const position = this.camera.getWorldPosition(new THREE.Vector3()).addScaledVector(direction, 2); + const mesh = sphereMesh.clone(); + mesh.position.copy(position); + const collider = new THREE.Sphere(position, sphereRadius); + const velocity = new THREE.Vector3().addScaledVector(direction, 80); + + const solidSphere: SolidSphere = { mesh, collider, velocity }; + this.scene.add(solidSphere.mesh); + this.spheres.push(solidSphere); + } + }); } protected override update(): void { + this.updateRay(); + this.updateSpheres(); + } + + private updateRay(): void { const maxDistance = 500; const rayFrom = this.ray.group.getWorldPosition(new THREE.Vector3()); const rayDirection = new THREE.Vector3(0, 1, 0).transformDirection(this.ray.group.matrixWorld); @@ -120,6 +153,31 @@ class TestPhysics extends TestBase { this.setRayLength(intersectionDistance); } + private updateSpheres(): void { + const now = performance.now(); + const lastUpdate = this.lastUpdate ?? now; + const deltaTime = (now - lastUpdate) / 1000; + this.lastUpdate = now; + + const gravity = 80; + + for (const sphere of this.spheres) { + sphere.collider.center.addScaledVector(sphere.velocity, deltaTime); + sphere.mesh.position.copy(sphere.collider.center); + + const result = this.voxelmapCollisions.sphereIntersect(sphere.collider); + if (result) { + sphere.velocity.addScaledVector(result.normal, - result.normal.dot(sphere.velocity) * 1.5); + sphere.collider.center.add(result.normal.multiplyScalar(result.depth)); + } else { + sphere.velocity.y -= gravity * deltaTime; + } + + const damping = Math.exp(-0.5 * deltaTime) - 1; + sphere.velocity.addScaledVector(sphere.velocity, damping); + } + } + private async displayMap(): Promise { const patchesToDisplay = this.voxelmapVisibilityComputer.getRequestedPatches(); const patchesIdToDisplay = patchesToDisplay.map(patchToDisplay => patchToDisplay.id); From 00dca1774a8aa98a56924b1f63406844d3e36b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 14 Oct 2024 22:41:23 +0200 Subject: [PATCH 02/16] test: player controller --- src/test/test-physics.ts | 84 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index d1958a8a..3da8e7ca 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -12,6 +12,7 @@ import { } from '../lib'; import { TestBase } from './test-base'; +import GUI from 'lil-gui'; type SolidSphere = { readonly mesh: THREE.Mesh; @@ -20,6 +21,9 @@ type SolidSphere = { }; class TestPhysics extends TestBase { + private readonly parameters = { + playerMode: false, + }; private readonly map: IVoxelMap; private readonly voxelmapViewer: VoxelmapViewer; @@ -37,8 +41,17 @@ class TestPhysics extends TestBase { private readonly spheres: SolidSphere[] = []; + private readonly player: { + readonly mesh: THREE.Mesh; + readonly collider: THREE.Sphere; + readonly velocity: THREE.Vector3; + touchesFloor: boolean; + }; + private lastUpdate: number | null = null; + private readonly keyDown = new Map(); + public constructor(map: IVoxelMap) { super(); @@ -119,6 +132,15 @@ class TestPhysics extends TestBase { this.setRayLength(10); + const playerSphereRadius = 1.1; + this.player = { + mesh: new THREE.Mesh(new THREE.SphereGeometry(playerSphereRadius), new THREE.MeshPhongMaterial({ color: 0xdddddd })), + collider: new THREE.Sphere(new THREE.Vector3(0.5, 160, 0.5), playerSphereRadius), + velocity: new THREE.Vector3(0, 0, 0), + touchesFloor: false, + }; + this.scene.add(this.player.mesh); + const sphereRadius = 1.1; const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(sphereRadius), new THREE.MeshPhongMaterial({ color: 0xdddddd })); @@ -135,12 +157,17 @@ class TestPhysics extends TestBase { this.scene.add(solidSphere.mesh); this.spheres.push(solidSphere); } + + this.keyDown.set(event.code, false); + }); + window.addEventListener("keydown", event => { + this.keyDown.set(event.code, true); }); } protected override update(): void { this.updateRay(); - this.updateSpheres(); + this.updateSpheresAndPlayer(); } private updateRay(): void { @@ -153,7 +180,7 @@ class TestPhysics extends TestBase { this.setRayLength(intersectionDistance); } - private updateSpheres(): void { + private updateSpheresAndPlayer(): void { const now = performance.now(); const lastUpdate = this.lastUpdate ?? now; const deltaTime = (now - lastUpdate) / 1000; @@ -176,6 +203,59 @@ class TestPhysics extends TestBase { const damping = Math.exp(-0.5 * deltaTime) - 1; sphere.velocity.addScaledVector(sphere.velocity, damping); } + + { + this.player.collider.center.addScaledVector(this.player.velocity, deltaTime); + this.player.mesh.position.copy(this.player.collider.center); + + this.player.touchesFloor = false; + + const result = this.voxelmapCollisions.sphereIntersect(this.player.collider); + if (result) { + // result.normal.x *= 0.5; + // result.normal.z *= 0.5; + this.player.velocity.addScaledVector(result.normal, - result.normal.dot(this.player.velocity) * 1.1); + this.player.collider.center.add(result.normal.multiplyScalar(result.depth)); + if (result.normal.y > 0) { + this.player.touchesFloor = true; + } + } else { + this.player.velocity.y -= gravity * deltaTime; + } + + const damping = Math.exp(-1 * deltaTime) - 1; + this.player.velocity.addScaledVector(this.player.velocity, damping); + + if (this.player.touchesFloor) { + const directiond2d = new THREE.Vector2(0, 0); + if (this.keyDown.get("KeyW")) { + directiond2d.y++; + } + if (this.keyDown.get("KeyS")) { + directiond2d.y--; + } + if (this.keyDown.get("KeyA")) { + directiond2d.x--; + } + if (this.keyDown.get("KeyD")) { + directiond2d.x++; + } + + const cameraFront = new THREE.Vector3(0, 0, -1) + .applyQuaternion(this.camera.quaternion) + .setY(0) + .normalize(); + const cameraRight = new THREE.Vector3(1, 0, 0) + .applyQuaternion(this.camera.quaternion) + .setY(0) + .normalize() + + directiond2d.normalize().multiplyScalar(100 * deltaTime); + this.player.velocity.addScaledVector(cameraRight, directiond2d.x).addScaledVector(cameraFront, directiond2d.y); + + // this.cameraControl.target.copy(this.player.collider.center); + } + } } private async displayMap(): Promise { From 76c7d526f5f3d191a90c30ead637c748eb454bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Wed, 16 Oct 2024 13:01:14 +0200 Subject: [PATCH 03/16] test: implement FPS limit To see how the physics engine behaves when FPS is low (=> deltaT is high) --- src/test/test-base.ts | 21 ++++++++++++++++----- src/test/test-physics.ts | 9 +++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/test/test-base.ts b/src/test/test-base.ts index 221ea87f..c9c6255b 100644 --- a/src/test/test-base.ts +++ b/src/test/test-base.ts @@ -10,6 +10,8 @@ abstract class TestBase { private started: boolean = false; + protected maxFps: number = Infinity; + public constructor() { this.stats = new THREE.Stats(); document.body.appendChild(this.stats.dom); @@ -46,12 +48,21 @@ abstract class TestBase { } this.started = true; - const render = () => { - this.stats.update(); + let lastRenderTimestamp = performance.now(); - this.cameraControl.update(); - this.update(); - this.renderer.render(this.scene, this.camera); + const render = () => { + const now = performance.now(); + const minDeltaTime = 1000 / this.maxFps; + const deltaTime = now - lastRenderTimestamp; + + if (deltaTime >= minDeltaTime) { + this.stats.update(); + + this.cameraControl.update(); + this.update(); + this.renderer.render(this.scene, this.camera); + lastRenderTimestamp = now; + } window.requestAnimationFrame(render); }; window.requestAnimationFrame(render); diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index 3da8e7ca..4f235bbe 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -1,4 +1,5 @@ import * as THREE from 'three-usage-test'; +import GUI from 'lil-gui'; import { EComputationMethod, @@ -12,7 +13,7 @@ import { } from '../lib'; import { TestBase } from './test-base'; -import GUI from 'lil-gui'; + type SolidSphere = { readonly mesh: THREE.Mesh; @@ -21,9 +22,6 @@ type SolidSphere = { }; class TestPhysics extends TestBase { - private readonly parameters = { - playerMode: false, - }; private readonly map: IVoxelMap; private readonly voxelmapViewer: VoxelmapViewer; @@ -55,6 +53,9 @@ class TestPhysics extends TestBase { public constructor(map: IVoxelMap) { super(); + const gui = new GUI(); + gui.add(this, "maxFps", 1, 120, 1); + this.camera.position.set(50, 200, 50); this.cameraControl.target.set(0, 170, 0); From 06a203a2ea14853b026e05831c2b548b663c3e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Wed, 16 Oct 2024 13:43:52 +0200 Subject: [PATCH 04/16] test: fixed precision for physics engine --- src/test/test-physics.ts | 106 ++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index 4f235bbe..100c2111 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -187,6 +187,18 @@ class TestPhysics extends TestBase { const deltaTime = (now - lastUpdate) / 1000; this.lastUpdate = now; + const maxDeltaTime = 10 / 1000; + let remainingDeltaTime = deltaTime; + while (remainingDeltaTime > 0) { + const localDeltaTime = Math.min(remainingDeltaTime, maxDeltaTime); + this.updateSpheres(localDeltaTime); + this.updatePlayer(localDeltaTime); + console.log(localDeltaTime); + remainingDeltaTime -= localDeltaTime; + } + } + + private updateSpheres(deltaTime: number): void { const gravity = 80; for (const sphere of this.spheres) { @@ -204,58 +216,60 @@ class TestPhysics extends TestBase { const damping = Math.exp(-0.5 * deltaTime) - 1; sphere.velocity.addScaledVector(sphere.velocity, damping); } + } - { - this.player.collider.center.addScaledVector(this.player.velocity, deltaTime); - this.player.mesh.position.copy(this.player.collider.center); + private updatePlayer(deltaTime: number): void { + const gravity = 80; - this.player.touchesFloor = false; + this.player.collider.center.addScaledVector(this.player.velocity, deltaTime); + this.player.mesh.position.copy(this.player.collider.center); - const result = this.voxelmapCollisions.sphereIntersect(this.player.collider); - if (result) { - // result.normal.x *= 0.5; - // result.normal.z *= 0.5; - this.player.velocity.addScaledVector(result.normal, - result.normal.dot(this.player.velocity) * 1.1); - this.player.collider.center.add(result.normal.multiplyScalar(result.depth)); - if (result.normal.y > 0) { - this.player.touchesFloor = true; - } - } else { - this.player.velocity.y -= gravity * deltaTime; + this.player.touchesFloor = false; + + const result = this.voxelmapCollisions.sphereIntersect(this.player.collider); + if (result) { + // result.normal.x *= 0.5; + // result.normal.z *= 0.5; + this.player.velocity.addScaledVector(result.normal, - result.normal.dot(this.player.velocity) * 1.1); + this.player.collider.center.add(result.normal.multiplyScalar(result.depth)); + if (result.normal.y > 0) { + this.player.touchesFloor = true; } + } else { + this.player.velocity.y -= gravity * deltaTime; + } + + const damping = Math.exp(-1 * deltaTime) - 1; + this.player.velocity.addScaledVector(this.player.velocity, damping); - const damping = Math.exp(-1 * deltaTime) - 1; - this.player.velocity.addScaledVector(this.player.velocity, damping); - - if (this.player.touchesFloor) { - const directiond2d = new THREE.Vector2(0, 0); - if (this.keyDown.get("KeyW")) { - directiond2d.y++; - } - if (this.keyDown.get("KeyS")) { - directiond2d.y--; - } - if (this.keyDown.get("KeyA")) { - directiond2d.x--; - } - if (this.keyDown.get("KeyD")) { - directiond2d.x++; - } - - const cameraFront = new THREE.Vector3(0, 0, -1) - .applyQuaternion(this.camera.quaternion) - .setY(0) - .normalize(); - const cameraRight = new THREE.Vector3(1, 0, 0) - .applyQuaternion(this.camera.quaternion) - .setY(0) - .normalize() - - directiond2d.normalize().multiplyScalar(100 * deltaTime); - this.player.velocity.addScaledVector(cameraRight, directiond2d.x).addScaledVector(cameraFront, directiond2d.y); - - // this.cameraControl.target.copy(this.player.collider.center); + if (this.player.touchesFloor) { + const directiond2d = new THREE.Vector2(0, 0); + if (this.keyDown.get("KeyW")) { + directiond2d.y++; + } + if (this.keyDown.get("KeyS")) { + directiond2d.y--; + } + if (this.keyDown.get("KeyA")) { + directiond2d.x--; } + if (this.keyDown.get("KeyD")) { + directiond2d.x++; + } + + const cameraFront = new THREE.Vector3(0, 0, -1) + .applyQuaternion(this.camera.quaternion) + .setY(0) + .normalize(); + const cameraRight = new THREE.Vector3(1, 0, 0) + .applyQuaternion(this.camera.quaternion) + .setY(0) + .normalize() + + directiond2d.normalize().multiplyScalar(100 * deltaTime); + this.player.velocity.addScaledVector(cameraRight, directiond2d.x).addScaledVector(cameraFront, directiond2d.y); + + // this.cameraControl.target.copy(this.player.collider.center); } } From d23b558d21a0a3e2abf9cb9aa3de35b07929b3ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Thu, 17 Oct 2024 16:07:44 +0200 Subject: [PATCH 05/16] feat: base player algorithm --- src/lib/index.ts | 2 +- src/lib/physics/voxelmap-collisions.ts | 38 ++++-- src/test/libs/three-usage-test.ts | 6 +- src/test/test-base.ts | 4 +- src/test/test-physics.ts | 177 ++++++++++++++++++------- 5 files changed, 158 insertions(+), 69 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 41d3d39a..19a3f200 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -30,7 +30,7 @@ export { export { VoxelmapVisibilityComputer } from './terrain/voxelmap/voxelmap-visibility-computer'; export { type CheckerboardType } from './terrain/voxelmap/voxelsRenderable/voxelsRenderableFactory/voxels-renderable-factory-base'; -export { VoxelmapCollider } from './physics/voxelmap-collider'; +export { EVoxelStatus, VoxelmapCollider } from './physics/voxelmap-collider'; export { VoxelmapCollisions } from './physics/voxelmap-collisions'; export { InstancedBillboard } from './effects/billboard/instanced-billboard'; diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 20d27eab..1f06c70b 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -120,18 +120,33 @@ class VoxelmapCollisions { const sphereCenterLocalProjection2dY = this.pointSquareProjection({ x: localSphereCenter.x, y: localSphereCenter.z }); const sphereCenterLocalProjection2dZ = this.pointSquareProjection({ x: localSphereCenter.x, y: localSphereCenter.y }); - const sphereCenterProjection2dX = { x: sphereCenterLocalProjection2dX.x + voxel.z, y: sphereCenterLocalProjection2dX.y + voxel.y }; - const sphereCenterProjection2dY = { x: sphereCenterLocalProjection2dY.x + voxel.x, y: sphereCenterLocalProjection2dY.y + voxel.z }; - const sphereCenterProjection2dZ = { x: sphereCenterLocalProjection2dZ.x + voxel.x, y: sphereCenterLocalProjection2dZ.y + voxel.y }; + const sphereCenterProjection2dX = { + x: sphereCenterLocalProjection2dX.x + voxel.z, + y: sphereCenterLocalProjection2dX.y + voxel.y, + }; + const sphereCenterProjection2dY = { + x: sphereCenterLocalProjection2dY.x + voxel.x, + y: sphereCenterLocalProjection2dY.y + voxel.z, + }; + const sphereCenterProjection2dZ = { + x: sphereCenterLocalProjection2dZ.x + voxel.x, + y: sphereCenterLocalProjection2dZ.y + voxel.y, + }; const voxelIsFull = this.voxelmapCollider.getVoxel(voxel) !== EVoxelStatus.EMPTY; - const voxelBelowIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y - 1, z: voxel.z }) !== EVoxelStatus.EMPTY; - const voxelAboveIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y + 1, z: voxel.z }) !== EVoxelStatus.EMPTY; - const voxelLeftIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x - 1, y: voxel.y, z: voxel.z }) !== EVoxelStatus.EMPTY; - const voxelRightIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x + 1, y: voxel.y, z: voxel.z }) !== EVoxelStatus.EMPTY; - const voxelBackIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y, z: voxel.z - 1 }) !== EVoxelStatus.EMPTY; - const voxelFrontIsFull = this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y, z: voxel.z + 1 }) !== EVoxelStatus.EMPTY; + const voxelBelowIsFull = + this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y - 1, z: voxel.z }) !== EVoxelStatus.EMPTY; + const voxelAboveIsFull = + this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y + 1, z: voxel.z }) !== EVoxelStatus.EMPTY; + const voxelLeftIsFull = + this.voxelmapCollider.getVoxel({ x: voxel.x - 1, y: voxel.y, z: voxel.z }) !== EVoxelStatus.EMPTY; + const voxelRightIsFull = + this.voxelmapCollider.getVoxel({ x: voxel.x + 1, y: voxel.y, z: voxel.z }) !== EVoxelStatus.EMPTY; + const voxelBackIsFull = + this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y, z: voxel.z - 1 }) !== EVoxelStatus.EMPTY; + const voxelFrontIsFull = + this.voxelmapCollider.getVoxel({ x: voxel.x, y: voxel.y, z: voxel.z + 1 }) !== EVoxelStatus.EMPTY; if (voxelIsFull !== voxelBelowIsFull) { addDisplacementIfNeeded({ x: sphereCenterProjection2dY.x, y: voxel.y, z: sphereCenterProjection2dY.y }); @@ -173,10 +188,7 @@ class VoxelmapCollisions { /* Computes the projection of a point onto the {0,1}² square. */ private pointSquareProjection(point: THREE.Vector2Like): THREE.Vector2 { - return new THREE.Vector2( - clamp(point.x, 0, 1), - clamp(point.y, 0, 1), - ); + return new THREE.Vector2(clamp(point.x, 0, 1), clamp(point.y, 0, 1)); } } diff --git a/src/test/libs/three-usage-test.ts b/src/test/libs/three-usage-test.ts index a8a24504..93254c36 100644 --- a/src/test/libs/three-usage-test.ts +++ b/src/test/libs/three-usage-test.ts @@ -1,5 +1,4 @@ import Stats from 'three/examples/jsm/libs/stats.module.js'; - export { AdditiveBlending, AmbientLight, @@ -13,14 +12,15 @@ export { Matrix4, PCFShadowMap, PerspectiveCamera, + SRGBColorSpace, Scene, SphereGeometry, - SRGBColorSpace, WebGLRenderer, } from 'three'; export { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; export { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; -export { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; export { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; +export { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; +export { clamp } from 'three/src/math/MathUtils.js'; export * from '../../lib/libs/three-usage'; export { Stats }; diff --git a/src/test/test-base.ts b/src/test/test-base.ts index c9c6255b..b10bba72 100644 --- a/src/test/test-base.ts +++ b/src/test/test-base.ts @@ -54,10 +54,10 @@ abstract class TestBase { const now = performance.now(); const minDeltaTime = 1000 / this.maxFps; const deltaTime = now - lastRenderTimestamp; - + if (deltaTime >= minDeltaTime) { this.stats.update(); - + this.cameraControl.update(); this.update(); this.renderer.render(this.scene, this.camera); diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index 100c2111..547cd710 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -1,8 +1,9 @@ -import * as THREE from 'three-usage-test'; import GUI from 'lil-gui'; +import * as THREE from 'three-usage-test'; import { EComputationMethod, + EVoxelStatus, type IVoxelMap, PromisesQueue, VoxelmapCollider, @@ -14,7 +15,6 @@ import { import { TestBase } from './test-base'; - type SolidSphere = { readonly mesh: THREE.Mesh; readonly collider: THREE.Sphere; @@ -40,8 +40,12 @@ class TestPhysics extends TestBase { private readonly spheres: SolidSphere[] = []; private readonly player: { - readonly mesh: THREE.Mesh; - readonly collider: THREE.Sphere; + readonly size: { + readonly radius: number; + readonly height: number; + }; + + readonly container: THREE.Object3D; readonly velocity: THREE.Vector3; touchesFloor: boolean; }; @@ -54,10 +58,10 @@ class TestPhysics extends TestBase { super(); const gui = new GUI(); - gui.add(this, "maxFps", 1, 120, 1); + gui.add(this, 'maxFps', 1, 120, 1); - this.camera.position.set(50, 200, 50); - this.cameraControl.target.set(0, 170, 0); + this.camera.position.set(-10, 170, 0); + this.cameraControl.target.set(0, 150, 0); const ambientLight = new THREE.AmbientLight(0xffffff, 2); this.scene.add(ambientLight); @@ -85,6 +89,7 @@ class TestPhysics extends TestBase { checkerboardType: 'xz', voxelsChunkOrdering, }); + this.scene.add(this.voxelmapViewer.container); this.promisesQueue = new PromisesQueue(this.voxelmapViewer.maxPatchesComputedInParallel + 5); @@ -99,7 +104,7 @@ class TestPhysics extends TestBase { minChunkIdY, maxChunkIdY ); - this.voxelmapVisibilityComputer.showMapAroundPosition({ x: 0, y: 0, z: 0 }, 500); + this.voxelmapVisibilityComputer.showMapAroundPosition({ x: 0, y: 0, z: 0 }, 200); this.displayMap(); @@ -133,20 +138,32 @@ class TestPhysics extends TestBase { this.setRayLength(10); - const playerSphereRadius = 1.1; + const playerSize = { + radius: 0.2, + height: 1.5, + }; + const playerMesh = new THREE.Mesh( + new THREE.CylinderGeometry(playerSize.radius, playerSize.radius, playerSize.height), + new THREE.MeshPhongMaterial({ color: 0xdddddd }) + ); + const playerContainer = new THREE.Group(); + playerContainer.add(playerMesh); + playerMesh.position.y = playerSize.height / 2; + playerContainer.position.y = 160; + this.player = { - mesh: new THREE.Mesh(new THREE.SphereGeometry(playerSphereRadius), new THREE.MeshPhongMaterial({ color: 0xdddddd })), - collider: new THREE.Sphere(new THREE.Vector3(0.5, 160, 0.5), playerSphereRadius), + size: playerSize, + container: playerContainer, velocity: new THREE.Vector3(0, 0, 0), touchesFloor: false, }; - this.scene.add(this.player.mesh); + this.scene.add(this.player.container); const sphereRadius = 1.1; const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(sphereRadius), new THREE.MeshPhongMaterial({ color: 0xdddddd })); - window.addEventListener("keyup", event => { - if (event.code === "Space") { + window.addEventListener('keyup', event => { + if (event.code === 'Space') { const direction = this.camera.getWorldDirection(new THREE.Vector3()); const position = this.camera.getWorldPosition(new THREE.Vector3()).addScaledVector(direction, 2); const mesh = sphereMesh.clone(); @@ -161,7 +178,7 @@ class TestPhysics extends TestBase { this.keyDown.set(event.code, false); }); - window.addEventListener("keydown", event => { + window.addEventListener('keydown', event => { this.keyDown.set(event.code, true); }); } @@ -193,7 +210,6 @@ class TestPhysics extends TestBase { const localDeltaTime = Math.min(remainingDeltaTime, maxDeltaTime); this.updateSpheres(localDeltaTime); this.updatePlayer(localDeltaTime); - console.log(localDeltaTime); remainingDeltaTime -= localDeltaTime; } } @@ -207,7 +223,7 @@ class TestPhysics extends TestBase { const result = this.voxelmapCollisions.sphereIntersect(sphere.collider); if (result) { - sphere.velocity.addScaledVector(result.normal, - result.normal.dot(sphere.velocity) * 1.5); + sphere.velocity.addScaledVector(result.normal, -result.normal.dot(sphere.velocity) * 1.5); sphere.collider.center.add(result.normal.multiplyScalar(result.depth)); } else { sphere.velocity.y -= gravity * deltaTime; @@ -219,57 +235,118 @@ class TestPhysics extends TestBase { } private updatePlayer(deltaTime: number): void { - const gravity = 80; + const gravity = 20; + const movementSpeed = 10; + const ascendSpeed = 10; + + const playerPosition = this.player.container.position; + const playerVelocity = this.player.velocity; + const previousPosition = playerPosition.clone(); + + const fromX = Math.floor(playerPosition.x - this.player.size.radius); + const toX = Math.floor(playerPosition.x + this.player.size.radius); + const fromZ = Math.floor(playerPosition.z - this.player.size.radius); + const toZ = Math.floor(playerPosition.z + this.player.size.radius); + + playerPosition.addScaledVector(playerVelocity, deltaTime); + + const isXZRelevant = (voxelX: number, voxelZ: number) => { + const projection = { + x: THREE.clamp(playerPosition.x, voxelX, voxelX + 1), + z: THREE.clamp(playerPosition.z, voxelZ, voxelZ + 1), + }; + const toCenter = { + x: projection.x - playerPosition.x, + z: projection.z - playerPosition.z, + }; + const distance = Math.sqrt(toCenter.x ** 2 + toCenter.z ** 2); + return distance < this.player.size.radius; + }; + + const isLevelFree = (y: number) => { + for (let iX = fromX; iX <= toX; iX++) { + for (let iZ = fromZ; iZ <= toZ; iZ++) { + if (isXZRelevant(iX, iZ)) { + if (this.voxelmapCollider.getVoxel({ x: iX, y, z: iZ }) !== EVoxelStatus.EMPTY) { + return false; + } + } + } + } + return true; + }; - this.player.collider.center.addScaledVector(this.player.velocity, deltaTime); - this.player.mesh.position.copy(this.player.collider.center); + const previousLevel = Math.floor(previousPosition.y); + const newLevel = Math.floor(playerPosition.y); - this.player.touchesFloor = false; + if (newLevel < previousLevel && !isLevelFree(previousLevel - 1)) { + // we just entered the ground -> rollback + playerVelocity.y = 0; + playerPosition.y = previousLevel; + } - const result = this.voxelmapCollisions.sphereIntersect(this.player.collider); - if (result) { - // result.normal.x *= 0.5; - // result.normal.z *= 0.5; - this.player.velocity.addScaledVector(result.normal, - result.normal.dot(this.player.velocity) * 1.1); - this.player.collider.center.add(result.normal.multiplyScalar(result.depth)); - if (result.normal.y > 0) { - this.player.touchesFloor = true; - } + const levelBelow = Number.isInteger(playerPosition.y) ? playerPosition.y - 1 : Math.floor(playerPosition.y); + const belowIsEmpty = isLevelFree(levelBelow); + if (belowIsEmpty) { + playerVelocity.y = -gravity; } else { - this.player.velocity.y -= gravity * deltaTime; - } + playerVelocity.y = 0; + + let isAscending = false; + const currentLevel = Math.floor(playerPosition.y); + if (!isLevelFree(currentLevel)) { + // we are partially in the map + + let aboveLevelsAreFree = true; + const aboveLevelsFrom = currentLevel + 1; + const aboveLevelsTo = Math.floor(aboveLevelsFrom + this.player.size.height); + for (let iY = aboveLevelsFrom; iY <= aboveLevelsTo; iY++) { + if (!isLevelFree(iY)) { + aboveLevelsAreFree = false; + break; + } + } - const damping = Math.exp(-1 * deltaTime) - 1; - this.player.velocity.addScaledVector(this.player.velocity, damping); + if (aboveLevelsAreFree) { + isAscending = true; + } + } + if (isAscending) { + playerVelocity.y = ascendSpeed; + } else { + // TODO compute lateral collisions + } + } + + this.player.touchesFloor = true; if (this.player.touchesFloor) { + let isMoving = false; const directiond2d = new THREE.Vector2(0, 0); - if (this.keyDown.get("KeyW")) { + if (this.keyDown.get('KeyW')) { + isMoving = true; directiond2d.y++; } - if (this.keyDown.get("KeyS")) { + if (this.keyDown.get('KeyS')) { + isMoving = true; directiond2d.y--; } - if (this.keyDown.get("KeyA")) { + if (this.keyDown.get('KeyA')) { + isMoving = true; directiond2d.x--; } - if (this.keyDown.get("KeyD")) { + if (this.keyDown.get('KeyD')) { + isMoving = true; directiond2d.x++; } - const cameraFront = new THREE.Vector3(0, 0, -1) - .applyQuaternion(this.camera.quaternion) - .setY(0) - .normalize(); - const cameraRight = new THREE.Vector3(1, 0, 0) - .applyQuaternion(this.camera.quaternion) - .setY(0) - .normalize() - - directiond2d.normalize().multiplyScalar(100 * deltaTime); - this.player.velocity.addScaledVector(cameraRight, directiond2d.x).addScaledVector(cameraFront, directiond2d.y); + if (isMoving) { + const cameraFront = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion).setY(0).normalize(); + const cameraRight = new THREE.Vector3(1, 0, 0).applyQuaternion(this.camera.quaternion).setY(0).normalize(); - // this.cameraControl.target.copy(this.player.collider.center); + directiond2d.normalize().multiplyScalar(movementSpeed * deltaTime); + this.player.container.position.addScaledVector(cameraRight, directiond2d.x).addScaledVector(cameraFront, directiond2d.y); + } } } From ca22fcf2a6b2d7dc894a56a29f26abe806e65c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 21 Oct 2024 11:56:18 +0200 Subject: [PATCH 06/16] feat: base player collisions --- src/lib/physics/voxelmap-collisions.ts | 209 +++++++++++++++++++++++++ src/test/test-physics.ts | 114 +++----------- 2 files changed, 233 insertions(+), 90 deletions(-) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 1f06c70b..62790ced 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -16,6 +16,26 @@ type SphereIntersection = { readonly depth: number; }; +type EntityCollider = { + readonly radius: number; + readonly height: number; + readonly position: THREE.Vector3Like; + readonly velocity: THREE.Vector3Like; +}; + +type EntityCollisionOptions = { + readonly deltaTime: number; + readonly gravity: number; + readonly considerMissingVoxelAs: 'empty' | 'blocking'; +}; + +type EntityCollisionOutput = { + computationStatus: 'ok' | 'partial'; + position: THREE.Vector3; + velocity: THREE.Vector3; + isOnGround: boolean; +}; + function clamp(x: number, min: number, max: number): number { if (x < min) { return min; @@ -186,6 +206,195 @@ class VoxelmapCollisions { return null; } + public entityMovement(entityCollider: EntityCollider, options: EntityCollisionOptions): EntityCollisionOutput { + const ascendSpeed = 15; + + let allVoxelmapDataIsAvailable = true; + + const { deltaTime, gravity } = options; + if (gravity < 0) { + throw new Error(`Invert gravity not supported.`); + } + + const playerPosition = new THREE.Vector3().copy(entityCollider.position); + const playerVelocity = new THREE.Vector3().copy(entityCollider.velocity); + const playerRadius = entityCollider.radius; + const playerRadiusSquared = playerRadius * playerRadius; + const playerHeight = entityCollider.height; + const previousPosition = playerPosition.clone(); + + const fromX = Math.floor(playerPosition.x - playerRadius); + const toX = Math.floor(playerPosition.x + playerRadius); + const fromZ = Math.floor(playerPosition.z - playerRadius); + const toZ = Math.floor(playerPosition.z + playerRadius); + + playerPosition.addScaledVector(playerVelocity, deltaTime); + + const closetPointFromVoxel = (voxelX: number, voxelZ: number) => { + const projection = { + x: clamp(playerPosition.x, voxelX, voxelX + 1), + z: clamp(playerPosition.z, voxelZ, voxelZ + 1), + }; + return { + x: projection.x - playerPosition.x, + z: projection.z - playerPosition.z, + }; + }; + + const isXZRelevant = (voxelX: number, voxelZ: number) => { + const fromVoxel = closetPointFromVoxel(voxelX, voxelZ); + const distanceSquared = fromVoxel.x ** 2 + fromVoxel.z ** 2; + return distanceSquared < playerRadiusSquared; + }; + + let isVoxelFull: (voxel: THREE.Vector3Like) => boolean; + + if (options.considerMissingVoxelAs === 'blocking') { + isVoxelFull = (voxel: THREE.Vector3Like) => { + const voxelStatus = this.voxelmapCollider.getVoxel(voxel); + allVoxelmapDataIsAvailable &&= voxelStatus !== EVoxelStatus.NOT_LOADED; + return voxelStatus === EVoxelStatus.FULL || voxelStatus === EVoxelStatus.NOT_LOADED; + }; + } else if (options.considerMissingVoxelAs === 'empty') { + isVoxelFull = (voxel: THREE.Vector3Like) => { + const voxelStatus = this.voxelmapCollider.getVoxel(voxel); + allVoxelmapDataIsAvailable &&= voxelStatus !== EVoxelStatus.NOT_LOADED; + return voxelStatus === EVoxelStatus.FULL; + }; + } else { + throw new Error('Invalid parameter'); + } + + const isLevelFree = (y: number) => { + for (let iX = fromX; iX <= toX; iX++) { + for (let iZ = fromZ; iZ <= toZ; iZ++) { + if (isXZRelevant(iX, iZ) && isVoxelFull({ x: iX, y, z: iZ })) { + return false; + } + } + } + return true; + }; + + const previousLevel = Math.floor(previousPosition.y); + const newLevel = Math.floor(playerPosition.y); + + if (newLevel < previousLevel && !isLevelFree(previousLevel - 1)) { + // we just entered the ground -> rollback + playerVelocity.y = 0; + playerPosition.y = previousLevel; + } + + let isOnGround = false; + + const levelBelow = Number.isInteger(playerPosition.y) ? playerPosition.y - 1 : Math.floor(playerPosition.y); + const belowIsEmpty = isLevelFree(levelBelow); + if (belowIsEmpty) { + playerVelocity.y = -gravity; + } else { + playerVelocity.y = 0; + isOnGround = Number.isInteger(playerPosition.y); + + let isAscending = false; + const currentLevel = Math.floor(playerPosition.y); + if (!isLevelFree(currentLevel)) { + // we are partially in the map + let aboveLevelsAreFree = true; + const aboveLevelsFrom = currentLevel + 1; + const aboveLevelsTo = Math.floor(aboveLevelsFrom + playerHeight); + for (let iY = aboveLevelsFrom; iY <= aboveLevelsTo && aboveLevelsAreFree; iY++) { + if (!isLevelFree(iY)) { + aboveLevelsAreFree = false; + } + } + + if (aboveLevelsAreFree) { + isAscending = true; + } + } + + if (isAscending) { + playerVelocity.y = ascendSpeed; + } else { + const displacements: THREE.Vector3Like[] = []; + + type XZ = { readonly x: number; readonly z: number }; + const addDisplacement = (normal: XZ, projection: XZ) => { + const fromCenter = { x: projection.x - playerPosition.x, z: projection.z - playerPosition.z }; + const distanceSquared = fromCenter.x ** 2 + fromCenter.z ** 2; + if (distanceSquared < playerRadiusSquared) { + const distance = Math.sqrt(distanceSquared); + let depth: number; + if (fromCenter.x * normal.x + fromCenter.z * normal.z >= 0) { + depth = playerRadius + distance; + } else { + depth = playerRadius - distance; + } + displacements.push({ + x: normal.x * depth, + y: 0, + z: normal.z * depth, + }); + } + }; + + const levelFrom = Math.floor(playerPosition.y); + const levelTo = Math.floor(playerPosition.y + playerHeight); + const voxel = { x: 0, y: 0, z: 0 }; + for (voxel.y = levelFrom; voxel.y <= levelTo; voxel.y++) { + for (voxel.x = fromX; voxel.x <= toX; voxel.x++) { + for (voxel.z = fromZ; voxel.z <= toZ; voxel.z++) { + const isFull = isVoxelFull(voxel); + const isLeftFull = isVoxelFull({ x: voxel.x - 1, y: voxel.y, z: voxel.z }); + const isRightFull = isVoxelFull({ x: voxel.x + 1, y: voxel.y, z: voxel.z }); + const isBackFull = isVoxelFull({ x: voxel.x, y: voxel.y, z: voxel.z - 1 }); + const isFrontFull = isVoxelFull({ x: voxel.x, y: voxel.y, z: voxel.z + 1 }); + + if (isFull) { + if (!isLeftFull) { + const normal = { x: -1, z: 0 }; + const projection = { x: voxel.x, z: clamp(playerPosition.z, voxel.z, voxel.z + 1) }; + addDisplacement(normal, projection); + } + if (!isRightFull) { + const normal = { x: 1, z: 0 }; + const projection = { x: voxel.x + 1, z: clamp(playerPosition.z, voxel.z, voxel.z + 1) }; + addDisplacement(normal, projection); + } + if (!isBackFull) { + const normal = { x: 0, z: -1 }; + const projection = { x: clamp(playerPosition.x, voxel.x, voxel.x + 1), z: voxel.z }; + addDisplacement(normal, projection); + } + if (!isFrontFull) { + const normal = { x: 0, z: 1 }; + const projection = { x: clamp(playerPosition.x, voxel.x, voxel.x + 1), z: voxel.z + 1 }; + addDisplacement(normal, projection); + } + } + } + } + } + + if (displacements.length > 0) { + const averageDisplacement = new THREE.Vector3(); + for (const displacement of displacements) { + averageDisplacement.add(displacement); + } + averageDisplacement.divideScalar(displacements.length); + playerPosition.add(averageDisplacement); + } + } + } + + return { + computationStatus: allVoxelmapDataIsAvailable ? 'ok' : 'partial', + position: playerPosition, + velocity: playerVelocity, + isOnGround, + }; + } + /* Computes the projection of a point onto the {0,1}² square. */ private pointSquareProjection(point: THREE.Vector2Like): THREE.Vector2 { return new THREE.Vector2(clamp(point.x, 0, 1), clamp(point.y, 0, 1)); diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index 547cd710..135c9abd 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -3,7 +3,6 @@ import * as THREE from 'three-usage-test'; import { EComputationMethod, - EVoxelStatus, type IVoxelMap, PromisesQueue, VoxelmapCollider, @@ -47,7 +46,6 @@ class TestPhysics extends TestBase { readonly container: THREE.Object3D; readonly velocity: THREE.Vector3; - touchesFloor: boolean; }; private lastUpdate: number | null = null; @@ -140,7 +138,7 @@ class TestPhysics extends TestBase { const playerSize = { radius: 0.2, - height: 1.5, + height: 1.4, }; const playerMesh = new THREE.Mesh( new THREE.CylinderGeometry(playerSize.radius, playerSize.radius, playerSize.height), @@ -155,7 +153,6 @@ class TestPhysics extends TestBase { size: playerSize, container: playerContainer, velocity: new THREE.Vector3(0, 0, 0), - touchesFloor: false, }; this.scene.add(this.player.container); @@ -163,7 +160,7 @@ class TestPhysics extends TestBase { const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(sphereRadius), new THREE.MeshPhongMaterial({ color: 0xdddddd })); window.addEventListener('keyup', event => { - if (event.code === 'Space') { + if (event.code === 'KeyP') { const direction = this.camera.getWorldDirection(new THREE.Vector3()); const position = this.camera.getWorldPosition(new THREE.Vector3()).addScaledVector(direction, 2); const mesh = sphereMesh.clone(); @@ -235,92 +232,25 @@ class TestPhysics extends TestBase { } private updatePlayer(deltaTime: number): void { - const gravity = 20; - const movementSpeed = 10; - const ascendSpeed = 10; - - const playerPosition = this.player.container.position; - const playerVelocity = this.player.velocity; - const previousPosition = playerPosition.clone(); - - const fromX = Math.floor(playerPosition.x - this.player.size.radius); - const toX = Math.floor(playerPosition.x + this.player.size.radius); - const fromZ = Math.floor(playerPosition.z - this.player.size.radius); - const toZ = Math.floor(playerPosition.z + this.player.size.radius); - - playerPosition.addScaledVector(playerVelocity, deltaTime); - - const isXZRelevant = (voxelX: number, voxelZ: number) => { - const projection = { - x: THREE.clamp(playerPosition.x, voxelX, voxelX + 1), - z: THREE.clamp(playerPosition.z, voxelZ, voxelZ + 1), - }; - const toCenter = { - x: projection.x - playerPosition.x, - z: projection.z - playerPosition.z, - }; - const distance = Math.sqrt(toCenter.x ** 2 + toCenter.z ** 2); - return distance < this.player.size.radius; - }; - - const isLevelFree = (y: number) => { - for (let iX = fromX; iX <= toX; iX++) { - for (let iZ = fromZ; iZ <= toZ; iZ++) { - if (isXZRelevant(iX, iZ)) { - if (this.voxelmapCollider.getVoxel({ x: iX, y, z: iZ }) !== EVoxelStatus.EMPTY) { - return false; - } - } - } - } - return true; - }; - - const previousLevel = Math.floor(previousPosition.y); - const newLevel = Math.floor(playerPosition.y); - - if (newLevel < previousLevel && !isLevelFree(previousLevel - 1)) { - // we just entered the ground -> rollback - playerVelocity.y = 0; - playerPosition.y = previousLevel; - } - - const levelBelow = Number.isInteger(playerPosition.y) ? playerPosition.y - 1 : Math.floor(playerPosition.y); - const belowIsEmpty = isLevelFree(levelBelow); - if (belowIsEmpty) { - playerVelocity.y = -gravity; - } else { - playerVelocity.y = 0; - - let isAscending = false; - const currentLevel = Math.floor(playerPosition.y); - if (!isLevelFree(currentLevel)) { - // we are partially in the map - - let aboveLevelsAreFree = true; - const aboveLevelsFrom = currentLevel + 1; - const aboveLevelsTo = Math.floor(aboveLevelsFrom + this.player.size.height); - for (let iY = aboveLevelsFrom; iY <= aboveLevelsTo; iY++) { - if (!isLevelFree(iY)) { - aboveLevelsAreFree = false; - break; - } - } - - if (aboveLevelsAreFree) { - isAscending = true; - } + const entityCollisionOutput = this.voxelmapCollisions.entityMovement( + { + radius: this.player.size.radius, + height: this.player.size.height, + position: this.player.container.position, + velocity: this.player.velocity, + }, + { + deltaTime, + gravity: 20, + considerMissingVoxelAs: 'blocking', } + ); - if (isAscending) { - playerVelocity.y = ascendSpeed; - } else { - // TODO compute lateral collisions - } - } + this.player.container.position.copy(entityCollisionOutput.position); + this.player.velocity.copy(entityCollisionOutput.velocity); - this.player.touchesFloor = true; - if (this.player.touchesFloor) { + const movementSpeed = 10; + if (entityCollisionOutput.isOnGround) { let isMoving = false; const directiond2d = new THREE.Vector2(0, 0); if (this.keyDown.get('KeyW')) { @@ -344,8 +274,12 @@ class TestPhysics extends TestBase { const cameraFront = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion).setY(0).normalize(); const cameraRight = new THREE.Vector3(1, 0, 0).applyQuaternion(this.camera.quaternion).setY(0).normalize(); - directiond2d.normalize().multiplyScalar(movementSpeed * deltaTime); - this.player.container.position.addScaledVector(cameraRight, directiond2d.x).addScaledVector(cameraFront, directiond2d.y); + directiond2d.normalize().multiplyScalar(movementSpeed); + this.player.velocity.x = cameraRight.x * directiond2d.x + cameraFront.x * directiond2d.y; + this.player.velocity.z = cameraRight.z * directiond2d.x + cameraFront.z * directiond2d.y; + } else { + this.player.velocity.x = 0; + this.player.velocity.z = 0; } } } From 15541f04726b214bd65c2141c2bcaf3abb910eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 21 Oct 2024 12:19:40 +0200 Subject: [PATCH 07/16] refactor: extract interface IVoxelmapCollider --- src/lib/index.ts | 3 ++- src/lib/physics/i-voxelmap-collider.ts | 13 +++++++++++++ src/lib/physics/voxelmap-collider.ts | 10 +++------- src/lib/physics/voxelmap-collisions.ts | 7 ++++--- 4 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 src/lib/physics/i-voxelmap-collider.ts diff --git a/src/lib/index.ts b/src/lib/index.ts index 19a3f200..70ca3e4e 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -30,7 +30,8 @@ export { export { VoxelmapVisibilityComputer } from './terrain/voxelmap/voxelmap-visibility-computer'; export { type CheckerboardType } from './terrain/voxelmap/voxelsRenderable/voxelsRenderableFactory/voxels-renderable-factory-base'; -export { EVoxelStatus, VoxelmapCollider } from './physics/voxelmap-collider'; +export { EVoxelStatus, type IVoxelmapCollider } from './physics/i-voxelmap-collider'; +export { VoxelmapCollider } from './physics/voxelmap-collider'; export { VoxelmapCollisions } from './physics/voxelmap-collisions'; export { InstancedBillboard } from './effects/billboard/instanced-billboard'; diff --git a/src/lib/physics/i-voxelmap-collider.ts b/src/lib/physics/i-voxelmap-collider.ts new file mode 100644 index 00000000..6939d891 --- /dev/null +++ b/src/lib/physics/i-voxelmap-collider.ts @@ -0,0 +1,13 @@ +import type * as THREE from '../libs/three-usage'; + +enum EVoxelStatus { + EMPTY, + FULL, + NOT_LOADED, +} + +interface IVoxelmapCollider { + getVoxel(worldVoxelCoords: THREE.Vector3Like): EVoxelStatus; +} + +export { EVoxelStatus, type IVoxelmapCollider }; diff --git a/src/lib/physics/voxelmap-collider.ts b/src/lib/physics/voxelmap-collider.ts index 2784e3b3..66134adb 100644 --- a/src/lib/physics/voxelmap-collider.ts +++ b/src/lib/physics/voxelmap-collider.ts @@ -6,11 +6,7 @@ import { type VoxelsChunkOrdering } from '../terrain/voxelmap/i-voxelmap'; import { PatchId } from '../terrain/voxelmap/patch/patch-id'; import { VoxelmapDataPacking } from '../terrain/voxelmap/voxelmap-data-packing'; -enum EVoxelStatus { - EMPTY, - FULL, - NOT_LOADED, -} +import { EVoxelStatus, type IVoxelmapCollider } from './i-voxelmap-collider'; type ChunkData = { readonly data: Uint16Array; @@ -38,7 +34,7 @@ type Parameters = { readonly voxelsChunkOrdering: VoxelsChunkOrdering; }; -class VoxelmapCollider { +class VoxelmapCollider implements IVoxelmapCollider { private readonly chunkSize: THREE.Vector3Like; private readonly voxelsChunkOrdering: VoxelsChunkOrdering; private readonly indexFactors: THREE.Vector3Like; @@ -212,4 +208,4 @@ class VoxelmapCollider { } } -export { EVoxelStatus, VoxelmapCollider }; +export { VoxelmapCollider }; diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 62790ced..2ba2a163 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -1,9 +1,10 @@ +import { EVoxelStatus } from '..'; import * as THREE from '../libs/three-usage'; -import { EVoxelStatus, type VoxelmapCollider } from './voxelmap-collider'; +import { type IVoxelmapCollider } from './i-voxelmap-collider'; type Parameters = { - readonly voxelmapCollider: VoxelmapCollider; + readonly voxelmapCollider: IVoxelmapCollider; }; type RayIntersection = { @@ -46,7 +47,7 @@ function clamp(x: number, min: number, max: number): number { } class VoxelmapCollisions { - private readonly voxelmapCollider: VoxelmapCollider; + private readonly voxelmapCollider: IVoxelmapCollider; public constructor(params: Parameters) { this.voxelmapCollider = params.voxelmapCollider; From f1eea1d4c6cffb8295876efb1476d7cce08d3110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Wed, 23 Oct 2024 20:50:30 +0200 Subject: [PATCH 08/16] fix: don't throw error if same chunk is resubmitted twice --- src/lib/physics/voxelmap-collider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/physics/voxelmap-collider.ts b/src/lib/physics/voxelmap-collider.ts index 66134adb..10377a7d 100644 --- a/src/lib/physics/voxelmap-collider.ts +++ b/src/lib/physics/voxelmap-collider.ts @@ -127,7 +127,7 @@ class VoxelmapCollider implements IVoxelmapCollider { const patchId = new PatchId(chunkId); if (this.chunkCollidersMap[patchId.asString]) { - throw new Error(`Chunk "${patchId.asString}" already exists.`); + logger.warn(`Chunk "${patchId.asString}" already exists.`); } if (chunk.isEmpty) { From ed10105c82feca448904ce418a6a0ee2a0c0326d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Wed, 23 Oct 2024 20:54:37 +0200 Subject: [PATCH 09/16] fix: fix bug where player jumped when climbing block --- src/lib/physics/voxelmap-collisions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 2ba2a163..c6a0c2ed 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -315,7 +315,9 @@ class VoxelmapCollisions { } if (isAscending) { - playerVelocity.y = ascendSpeed; + const upwardsMovement = ascendSpeed * options.deltaTime; + const boundary = Number.isInteger(playerPosition.y) ? playerPosition.y + 1 : Math.ceil(playerPosition.y); + playerPosition.y = Math.min(boundary, playerPosition.y + upwardsMovement); } else { const displacements: THREE.Vector3Like[] = []; From 3372c7c04ec26a3862239dfb5cbde6db1d033858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Wed, 23 Oct 2024 21:40:50 +0200 Subject: [PATCH 10/16] fix: fix most glitches in physics --- src/lib/physics/voxelmap-collisions.ts | 67 +++++++++++++++++++++----- src/test/test-physics.ts | 4 +- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index c6a0c2ed..13ae9d3b 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -208,7 +208,46 @@ class VoxelmapCollisions { } public entityMovement(entityCollider: EntityCollider, options: EntityCollisionOptions): EntityCollisionOutput { - const ascendSpeed = 15; + const maxDeltaTime = 10 / 1000; + + let currentState = entityCollider; + const output: EntityCollisionOutput = { + computationStatus: "ok", + position: new THREE.Vector3().copy(entityCollider.position), + velocity: new THREE.Vector3().copy(entityCollider.velocity), + isOnGround: false, + }; + + let remainingDeltaTime = options.deltaTime; + while (remainingDeltaTime > 0) { + const localDeltaTime = Math.min(remainingDeltaTime, maxDeltaTime); + remainingDeltaTime -= localDeltaTime; + const localOutput = this.entityMovementInternal(currentState, { + ...options, + deltaTime: localDeltaTime, + }); + + currentState = { + radius: currentState.radius, + height: currentState.height, + position: localOutput.position, + velocity: currentState.velocity, + }; + + if (localOutput.computationStatus === "partial") { + output.computationStatus = "partial"; + } + output.position = localOutput.position; + output.velocity = localOutput.velocity; + output.isOnGround = localOutput.isOnGround; + } + + return output; + } + + private entityMovementInternal(entityCollider: EntityCollider, options: EntityCollisionOptions): EntityCollisionOutput { + const ascendSpeed = 10; + const epsilon = 1e-5; let allVoxelmapDataIsAvailable = true; @@ -249,7 +288,6 @@ class VoxelmapCollisions { }; let isVoxelFull: (voxel: THREE.Vector3Like) => boolean; - if (options.considerMissingVoxelAs === 'blocking') { isVoxelFull = (voxel: THREE.Vector3Like) => { const voxelStatus = this.voxelmapCollider.getVoxel(voxel); @@ -327,17 +365,15 @@ class VoxelmapCollisions { const distanceSquared = fromCenter.x ** 2 + fromCenter.z ** 2; if (distanceSquared < playerRadiusSquared) { const distance = Math.sqrt(distanceSquared); - let depth: number; - if (fromCenter.x * normal.x + fromCenter.z * normal.z >= 0) { - depth = playerRadius + distance; - } else { - depth = playerRadius - distance; + if (fromCenter.x * normal.x + fromCenter.z * normal.z < 0) { + const depth = playerRadius - distance + epsilon; + + displacements.push({ + x: normal.x * depth, + y: 0, + z: normal.z * depth, + }); } - displacements.push({ - x: normal.x * depth, - y: 0, - z: normal.z * depth, - }); } }; @@ -386,6 +422,13 @@ class VoxelmapCollisions { } averageDisplacement.divideScalar(displacements.length); playerPosition.add(averageDisplacement); + + if (averageDisplacement.x !== 0) { + playerVelocity.x = 0; + } + if (averageDisplacement.z !== 0) { + playerVelocity.z = 0; + } } } } diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index 135c9abd..ee7f0b42 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -206,9 +206,9 @@ class TestPhysics extends TestBase { while (remainingDeltaTime > 0) { const localDeltaTime = Math.min(remainingDeltaTime, maxDeltaTime); this.updateSpheres(localDeltaTime); - this.updatePlayer(localDeltaTime); remainingDeltaTime -= localDeltaTime; } + this.updatePlayer(deltaTime); } private updateSpheres(deltaTime: number): void { @@ -249,7 +249,7 @@ class TestPhysics extends TestBase { this.player.container.position.copy(entityCollisionOutput.position); this.player.velocity.copy(entityCollisionOutput.velocity); - const movementSpeed = 10; + const movementSpeed = 5; if (entityCollisionOutput.isOnGround) { let isMoving = false; const directiond2d = new THREE.Vector2(0, 0); From 2bf6dc3526accb54f41c7cf45ceab4655e73993d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 28 Oct 2024 13:55:52 +0100 Subject: [PATCH 11/16] test: better test scene --- src/test/test-physics.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index ee7f0b42..c6fba6af 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -51,6 +51,7 @@ class TestPhysics extends TestBase { private lastUpdate: number | null = null; private readonly keyDown = new Map(); + private readonly keysPressed: Set = new Set(); public constructor(map: IVoxelMap) { super(); @@ -174,6 +175,7 @@ class TestPhysics extends TestBase { } this.keyDown.set(event.code, false); + this.keysPressed.add(event.code); }); window.addEventListener('keydown', event => { this.keyDown.set(event.code, true); @@ -181,6 +183,12 @@ class TestPhysics extends TestBase { } protected override update(): void { + for (const [keyCode, isPressed] of this.keyDown.entries()) { + if (isPressed && keyCode !== "Space") { + this.keysPressed.add(keyCode); + } + } + this.updateRay(); this.updateSpheresAndPlayer(); } @@ -241,7 +249,7 @@ class TestPhysics extends TestBase { }, { deltaTime, - gravity: 20, + gravity: 250, considerMissingVoxelAs: 'blocking', } ); @@ -253,22 +261,26 @@ class TestPhysics extends TestBase { if (entityCollisionOutput.isOnGround) { let isMoving = false; const directiond2d = new THREE.Vector2(0, 0); - if (this.keyDown.get('KeyW')) { + if (this.keysPressed.has('KeyW')) { isMoving = true; directiond2d.y++; } - if (this.keyDown.get('KeyS')) { + if (this.keysPressed.has('KeyS')) { isMoving = true; directiond2d.y--; } - if (this.keyDown.get('KeyA')) { + if (this.keysPressed.has('KeyA')) { isMoving = true; directiond2d.x--; } - if (this.keyDown.get('KeyD')) { + if (this.keysPressed.has('KeyD')) { isMoving = true; directiond2d.x++; } + if (this.keysPressed.has("Space")) { + this.player.velocity.y = 20; + } + this.keysPressed.clear(); if (isMoving) { const cameraFront = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion).setY(0).normalize(); From 46d589fe3e011d1e6346e68ef97ba31a049d27a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 28 Oct 2024 14:03:18 +0100 Subject: [PATCH 12/16] feat: proper gravity --- src/lib/physics/voxelmap-collisions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 13ae9d3b..879446bb 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -329,7 +329,8 @@ class VoxelmapCollisions { const levelBelow = Number.isInteger(playerPosition.y) ? playerPosition.y - 1 : Math.floor(playerPosition.y); const belowIsEmpty = isLevelFree(levelBelow); if (belowIsEmpty) { - playerVelocity.y = -gravity; + playerVelocity.y -= gravity * deltaTime; + playerVelocity.y = Math.max(-gravity, playerVelocity.y); } else { playerVelocity.y = 0; isOnGround = Number.isInteger(playerPosition.y); From af02056d36758c234ec68866e9c898a10cd905a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 28 Oct 2024 14:03:38 +0100 Subject: [PATCH 13/16] fix: correctly apply gravity when framerate is low --- src/lib/physics/voxelmap-collisions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 879446bb..effdaa25 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -231,7 +231,7 @@ class VoxelmapCollisions { radius: currentState.radius, height: currentState.height, position: localOutput.position, - velocity: currentState.velocity, + velocity: localOutput.velocity, }; if (localOutput.computationStatus === "partial") { From a8ac6ae516ddd88d9be93305a269bdd6add2ef2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Sun, 3 Nov 2024 22:15:59 +0100 Subject: [PATCH 14/16] perf: improve physics stability --- src/lib/physics/voxelmap-collisions.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index effdaa25..3d69c34f 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -218,13 +218,10 @@ class VoxelmapCollisions { isOnGround: false, }; - let remainingDeltaTime = options.deltaTime; - while (remainingDeltaTime > 0) { - const localDeltaTime = Math.min(remainingDeltaTime, maxDeltaTime); - remainingDeltaTime -= localDeltaTime; + const applyAndMergeStep = (deltaTime: number) => { const localOutput = this.entityMovementInternal(currentState, { ...options, - deltaTime: localDeltaTime, + deltaTime, }); currentState = { @@ -240,6 +237,18 @@ class VoxelmapCollisions { output.position = localOutput.position; output.velocity = localOutput.velocity; output.isOnGround = localOutput.isOnGround; + }; + + let remainingDeltaTime = options.deltaTime; + while (remainingDeltaTime > 0) { + const localDeltaTime = Math.min(remainingDeltaTime, maxDeltaTime); + remainingDeltaTime -= localDeltaTime; + applyAndMergeStep(localDeltaTime); + } + + for (let i = 0; i < 3; i++) { + applyAndMergeStep(0); + applyAndMergeStep(0); } return output; From 6bf7d288b85f03b15f8e16274f1ae96d80cb50ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Sun, 3 Nov 2024 22:16:26 +0100 Subject: [PATCH 15/16] fix: fix bug where player could not jump when in front of a wall --- src/lib/physics/voxelmap-collisions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 3d69c34f..676b0c1c 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -341,7 +341,6 @@ class VoxelmapCollisions { playerVelocity.y -= gravity * deltaTime; playerVelocity.y = Math.max(-gravity, playerVelocity.y); } else { - playerVelocity.y = 0; isOnGround = Number.isInteger(playerPosition.y); let isAscending = false; From 628ddb58faa74080e245a61f1f6ea08fe165988b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Piellard?= Date: Mon, 4 Nov 2024 09:27:50 +0100 Subject: [PATCH 16/16] style: format --- src/lib/physics/voxelmap-collisions.ts | 6 +++--- src/test/test-physics.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/physics/voxelmap-collisions.ts b/src/lib/physics/voxelmap-collisions.ts index 676b0c1c..27dbcd61 100644 --- a/src/lib/physics/voxelmap-collisions.ts +++ b/src/lib/physics/voxelmap-collisions.ts @@ -212,7 +212,7 @@ class VoxelmapCollisions { let currentState = entityCollider; const output: EntityCollisionOutput = { - computationStatus: "ok", + computationStatus: 'ok', position: new THREE.Vector3().copy(entityCollider.position), velocity: new THREE.Vector3().copy(entityCollider.velocity), isOnGround: false, @@ -231,8 +231,8 @@ class VoxelmapCollisions { velocity: localOutput.velocity, }; - if (localOutput.computationStatus === "partial") { - output.computationStatus = "partial"; + if (localOutput.computationStatus === 'partial') { + output.computationStatus = 'partial'; } output.position = localOutput.position; output.velocity = localOutput.velocity; diff --git a/src/test/test-physics.ts b/src/test/test-physics.ts index c6fba6af..85f973de 100644 --- a/src/test/test-physics.ts +++ b/src/test/test-physics.ts @@ -184,7 +184,7 @@ class TestPhysics extends TestBase { protected override update(): void { for (const [keyCode, isPressed] of this.keyDown.entries()) { - if (isPressed && keyCode !== "Space") { + if (isPressed && keyCode !== 'Space') { this.keysPressed.add(keyCode); } } @@ -277,7 +277,7 @@ class TestPhysics extends TestBase { isMoving = true; directiond2d.x++; } - if (this.keysPressed.has("Space")) { + if (this.keysPressed.has('Space')) { this.player.velocity.y = 20; } this.keysPressed.clear();