@gg-web-engine/ammo 0.0.58 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build_gg_ammo/README.md +39 -7
- package/build_gg_ammo/ammo_patches.txt +56 -0
- package/build_gg_ammo/patch_files/CMakeLists.txt +121 -0
- package/build_gg_ammo/patch_files/Dockerfile +13 -0
- package/dist/ammo.js/ammo-ambient.d.ts +2 -7
- package/dist/ammo.js/ammo.d.ts +2 -7
- package/dist/ammo.js/ammo.js +1001 -36
- package/dist/ammo.js/ammo.wasm.js +988 -1313
- package/dist/ammo.js/ammo.wasm.wasm +0 -0
- package/dist/types.d.ts +3 -1
- package/package.json +14 -13
- package/test/ammo-factory.spec.ts +88 -0
- package/test/components/ammo-raycast-vehicle.component.spec.ts +139 -0
- package/test/components/ammo-world.component.spec.ts +50 -0
- package/tsconfig.json +5 -1
|
Binary file
|
package/dist/types.d.ts
CHANGED
|
@@ -12,4 +12,6 @@ export type AmmoPhysicsTypeDocRepo = {
|
|
|
12
12
|
trigger: AmmoTriggerComponent;
|
|
13
13
|
raycastVehicle: AmmoRaycastVehicleComponent;
|
|
14
14
|
};
|
|
15
|
-
export type
|
|
15
|
+
export type AmmoTypeDoc = Gg3dWorldTypeDocPPatch<AmmoPhysicsTypeDocRepo>;
|
|
16
|
+
export type AmmoSceneTypeDoc = Gg3dWorldSceneTypeDocPPatch<AmmoPhysicsTypeDocRepo, AmmoWorldComponent>;
|
|
17
|
+
export type AmmoGgWorld = Gg3dWorld<AmmoTypeDoc, AmmoSceneTypeDoc>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gg-web-engine/ammo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.60",
|
|
4
4
|
"description": "An attempt to create open source game engine for browser",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,20 +34,21 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@gg-web-engine/core": "0.0.
|
|
38
|
-
"@types/jest": "^
|
|
39
|
-
"
|
|
40
|
-
"jest
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
37
|
+
"@gg-web-engine/core": "0.0.60",
|
|
38
|
+
"@types/jest": "^30.0.0",
|
|
39
|
+
"@types/node": "^26.4.0",
|
|
40
|
+
"jest": "^30.5.0",
|
|
41
|
+
"jest-environment-jsdom": "^30.5.0",
|
|
42
|
+
"mini-signals": "3.0.0",
|
|
43
|
+
"prettier": "^3.9.6",
|
|
44
|
+
"rxjs": "7.8.2",
|
|
45
|
+
"ts-jest": "^29.4.12",
|
|
46
|
+
"typescript": "~6.0.3"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
|
-
"@gg-web-engine/core": "0.0.
|
|
49
|
-
"mini-signals": "
|
|
50
|
-
"rxjs": "7.8.
|
|
49
|
+
"@gg-web-engine/core": "0.0.60",
|
|
50
|
+
"mini-signals": "3.0.0",
|
|
51
|
+
"rxjs": "7.8.2"
|
|
51
52
|
},
|
|
52
53
|
"jest": {
|
|
53
54
|
"rootDir": ".",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Shape3DDescriptor } from '@gg-web-engine/core';
|
|
2
|
+
import { AmmoFactory, AmmoWorldComponent } from '../src';
|
|
3
|
+
|
|
4
|
+
describe('AmmoFactory', () => {
|
|
5
|
+
let world: AmmoWorldComponent;
|
|
6
|
+
let factory: AmmoFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
if (world) {
|
|
10
|
+
world.dispose();
|
|
11
|
+
}
|
|
12
|
+
world = new AmmoWorldComponent();
|
|
13
|
+
await world.init();
|
|
14
|
+
factory = world.factory;
|
|
15
|
+
world.gravity = { x: 0, y: 0, z: 0 };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
world.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const shapes: { name: string; shape: Shape3DDescriptor }[] = [
|
|
23
|
+
{ name: 'PLANE', shape: { shape: 'PLANE' } },
|
|
24
|
+
{ name: 'BOX', shape: { shape: 'BOX', dimensions: { x: 1, y: 1, z: 1 } } },
|
|
25
|
+
{ name: 'SPHERE', shape: { shape: 'SPHERE', radius: 1 } },
|
|
26
|
+
{ name: 'CAPSULE', shape: { shape: 'CAPSULE', radius: 0.5, centersDistance: 1 } },
|
|
27
|
+
{ name: 'CYLINDER', shape: { shape: 'CYLINDER', radius: 0.5, height: 1 } },
|
|
28
|
+
{ name: 'CONE', shape: { shape: 'CONE', radius: 0.5, height: 1 } },
|
|
29
|
+
{
|
|
30
|
+
name: 'COMPOUND',
|
|
31
|
+
shape: {
|
|
32
|
+
shape: 'COMPOUND',
|
|
33
|
+
children: [
|
|
34
|
+
{ shape: { shape: 'BOX', dimensions: { x: 1, y: 1, z: 1 } }, position: { x: 0, y: 0, z: 0 } },
|
|
35
|
+
{ shape: { shape: 'SPHERE', radius: 0.5 }, position: { x: 0, y: 0, z: 1 } },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'CONVEX_HULL',
|
|
41
|
+
shape: {
|
|
42
|
+
shape: 'CONVEX_HULL',
|
|
43
|
+
vertices: [
|
|
44
|
+
{ x: -1, y: -1, z: -1 },
|
|
45
|
+
{ x: 1, y: -1, z: -1 },
|
|
46
|
+
{ x: 1, y: 1, z: -1 },
|
|
47
|
+
{ x: -1, y: 1, z: -1 },
|
|
48
|
+
{ x: 0, y: 0, z: 1 },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'MESH',
|
|
54
|
+
shape: {
|
|
55
|
+
shape: 'MESH',
|
|
56
|
+
vertices: [
|
|
57
|
+
{ x: -1, y: -1, z: 0 },
|
|
58
|
+
{ x: 1, y: -1, z: 0 },
|
|
59
|
+
{ x: 0, y: 1, z: 0 },
|
|
60
|
+
],
|
|
61
|
+
faces: [[0, 1, 2]],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
describe.each(shapes)('$name shape', ({ shape }) => {
|
|
67
|
+
it('should create a static rigid body without throwing', () => {
|
|
68
|
+
const body = factory.createRigidBody({ shape, body: { dynamic: false, mass: 0 } }, { position: { x: 1, y: 2, z: 3 } });
|
|
69
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
70
|
+
expect(body.position).toEqual({ x: 1, y: 2, z: 3 });
|
|
71
|
+
world.simulate(16);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should create a trigger without throwing', () => {
|
|
75
|
+
const trigger = factory.createTrigger(shape, { position: { x: 0, y: 0, z: 0 } });
|
|
76
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
77
|
+
world.simulate(16);
|
|
78
|
+
trigger.checkOverlaps();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should throw a clear error for an unsupported shape', () => {
|
|
83
|
+
expect(() => factory.createRigidBody({
|
|
84
|
+
shape: { shape: 'NOT_A_SHAPE' } as any,
|
|
85
|
+
body: { dynamic: false, mass: 0 },
|
|
86
|
+
})).toThrow(/not implemented for Ammo\.js/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { SuspensionOptions, WheelOptions } from '@gg-web-engine/core';
|
|
2
|
+
import { AmmoFactory, AmmoRaycastVehicleComponent, AmmoRigidBodyComponent, AmmoWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
// Shared suspension/wheel setup mirroring examples/ammo-car-three-ammo, known to produce a
|
|
5
|
+
// stable, quickly-settling vehicle - see that example for the full car setup.
|
|
6
|
+
const suspension: SuspensionOptions = {
|
|
7
|
+
compression: 4.4,
|
|
8
|
+
damping: 2.3,
|
|
9
|
+
restLength: 0.6,
|
|
10
|
+
stiffness: 20,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const wheelOptions = (isFront: boolean, isLeft: boolean): WheelOptions => ({
|
|
14
|
+
isFront,
|
|
15
|
+
isLeft,
|
|
16
|
+
tyreRadius: isFront ? 0.35 : 0.4,
|
|
17
|
+
tyreWidth: isFront ? 0.2 : 0.3,
|
|
18
|
+
frictionSlip: 1000,
|
|
19
|
+
rollInfluence: 0.2,
|
|
20
|
+
maxTravel: 5,
|
|
21
|
+
position: {
|
|
22
|
+
x: isLeft ? 1 : -1,
|
|
23
|
+
y: isFront ? 1.7 : -1,
|
|
24
|
+
z: 0.3,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const addWheels = (vehicle: AmmoRaycastVehicleComponent) => {
|
|
29
|
+
for (const isFront of [true, false]) {
|
|
30
|
+
for (const isLeft of [true, false]) {
|
|
31
|
+
vehicle.addWheel(wheelOptions(isFront, isLeft), suspension);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const createFloor = (
|
|
37
|
+
factory: AmmoFactory,
|
|
38
|
+
topZ: number,
|
|
39
|
+
): AmmoRigidBodyComponent =>
|
|
40
|
+
factory.createRigidBody(
|
|
41
|
+
{
|
|
42
|
+
shape: { shape: 'BOX', dimensions: { x: 75, y: 75, z: 1 } },
|
|
43
|
+
body: { dynamic: false, mass: 0 },
|
|
44
|
+
},
|
|
45
|
+
{ position: { x: 0, y: 0, z: topZ - 0.5 } },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const createVehicle = (
|
|
49
|
+
world: AmmoWorldComponent,
|
|
50
|
+
factory: AmmoFactory,
|
|
51
|
+
position: { x: number; y: number; z: number },
|
|
52
|
+
): AmmoRaycastVehicleComponent => {
|
|
53
|
+
const chassis = factory.createRigidBody(
|
|
54
|
+
{ shape: { shape: 'BOX', dimensions: { x: 1.8, y: 4, z: 0.6 } }, body: { dynamic: true, mass: 800 } },
|
|
55
|
+
{ position },
|
|
56
|
+
);
|
|
57
|
+
const vehicle = factory.createRaycastVehicle(chassis);
|
|
58
|
+
addWheels(vehicle);
|
|
59
|
+
return vehicle;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe('AmmoRaycastVehicleComponent', () => {
|
|
63
|
+
let world: AmmoWorldComponent;
|
|
64
|
+
let factory: AmmoFactory;
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
if (world) {
|
|
68
|
+
world.dispose();
|
|
69
|
+
}
|
|
70
|
+
world = new AmmoWorldComponent();
|
|
71
|
+
await world.init();
|
|
72
|
+
factory = world.factory;
|
|
73
|
+
// default earth-like gravity, so vehicles actually fall onto the floors below
|
|
74
|
+
world.gravity = { x: 0, y: 0, z: -9.82 };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterAll(() => {
|
|
78
|
+
world.dispose();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should fall under gravity and come to rest on a floor below it', () => {
|
|
82
|
+
const floor = createFloor(factory, 0);
|
|
83
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
84
|
+
|
|
85
|
+
const vehicle = createVehicle(world, factory, { x: 0, y: 0, z: 4 });
|
|
86
|
+
vehicle.addToWorld({ physicsWorld: world } as any);
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < 100; i++) {
|
|
89
|
+
world.simulate(60);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// chassis should have settled just above the floor surface (z = 0), not still falling
|
|
93
|
+
// and not sunk through it
|
|
94
|
+
expect(vehicle.position.z).toBeGreaterThan(-1);
|
|
95
|
+
expect(vehicle.position.z).toBeLessThan(3);
|
|
96
|
+
expect(vehicle.isWheelTouchesGround(0)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should let each vehicle fall through a floor with a different collision group and rest ' +
|
|
100
|
+
'only on the floor sharing its own collision group', () => {
|
|
101
|
+
const groupA = world.registerCollisionGroup();
|
|
102
|
+
const groupB = world.registerCollisionGroup();
|
|
103
|
+
|
|
104
|
+
const floorA = createFloor(factory, 0);
|
|
105
|
+
floorA.addToWorld({ physicsWorld: world } as any);
|
|
106
|
+
floorA.ownCollisionGroups = floorA.interactWithCollisionGroups = [groupA];
|
|
107
|
+
|
|
108
|
+
const floorB = createFloor(factory, -10);
|
|
109
|
+
floorB.addToWorld({ physicsWorld: world } as any);
|
|
110
|
+
floorB.ownCollisionGroups = floorB.interactWithCollisionGroups = [groupB];
|
|
111
|
+
|
|
112
|
+
const vehicleA = createVehicle(world, factory, { x: 0, y: 0, z: 4 });
|
|
113
|
+
vehicleA.addToWorld({ physicsWorld: world } as any);
|
|
114
|
+
vehicleA.ownCollisionGroups = vehicleA.interactWithCollisionGroups = [groupA];
|
|
115
|
+
|
|
116
|
+
const vehicleB = createVehicle(world, factory, { x: 20, y: 0, z: 4 });
|
|
117
|
+
vehicleB.addToWorld({ physicsWorld: world } as any);
|
|
118
|
+
vehicleB.ownCollisionGroups = vehicleB.interactWithCollisionGroups = [groupB];
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < 100; i++) {
|
|
121
|
+
world.simulate(60);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// vehicleA shares its collision group with floorA (z=0) - it should have landed there,
|
|
125
|
+
// both via chassis-body collision and via the raycast vehicle's suspension.
|
|
126
|
+
expect(vehicleA.position.z).toBeGreaterThan(-1);
|
|
127
|
+
expect(vehicleA.position.z).toBeLessThan(3);
|
|
128
|
+
expect(vehicleA.isWheelTouchesGround(0)).toBe(true);
|
|
129
|
+
|
|
130
|
+
// vehicleB does not share a collision group with floorA, so it must fall straight through
|
|
131
|
+
// it (this is what the custom btVehicleRaycaster collision-group patch fixes: without it,
|
|
132
|
+
// the wheel raycast ignores collision groups entirely and would detect floorA as ground,
|
|
133
|
+
// holding the vehicle up via suspension force even though its chassis passes through).
|
|
134
|
+
// It should come to rest on floorB (z=-10) instead.
|
|
135
|
+
expect(vehicleB.position.z).toBeGreaterThan(-11);
|
|
136
|
+
expect(vehicleB.position.z).toBeLessThan(-7);
|
|
137
|
+
expect(vehicleB.isWheelTouchesGround(0)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -17,6 +17,56 @@ describe('AmmoWorldComponent', () => {
|
|
|
17
17
|
world.dispose();
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
describe('Gravity', () => {
|
|
21
|
+
it('should default to earth-like downward gravity on a fresh world', async () => {
|
|
22
|
+
const freshWorld = new AmmoWorldComponent();
|
|
23
|
+
await freshWorld.init();
|
|
24
|
+
expect(freshWorld.gravity).toEqual({ x: 0, y: 0, z: -9.82 });
|
|
25
|
+
freshWorld.dispose();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should read back a value written to it and apply it to the simulation', () => {
|
|
29
|
+
world.gravity = { x: 1, y: -2, z: 3 };
|
|
30
|
+
expect(world.gravity).toEqual({ x: 1, y: -2, z: 3 });
|
|
31
|
+
|
|
32
|
+
const ball = world.factory.createRigidBody({
|
|
33
|
+
shape: { shape: 'SPHERE', radius: 1 },
|
|
34
|
+
body: { dynamic: true, mass: 1 },
|
|
35
|
+
}, { position: { x: 0, y: 0, z: 0 } });
|
|
36
|
+
ball.addToWorld({ physicsWorld: world } as any);
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < 20; i++) {
|
|
39
|
+
world.simulate(60);
|
|
40
|
+
}
|
|
41
|
+
// ball should have drifted along every gravity axis in the sign of that axis' gravity
|
|
42
|
+
expect(ball.position.x).toBeGreaterThan(0);
|
|
43
|
+
expect(ball.position.y).toBeLessThan(0);
|
|
44
|
+
expect(ball.position.z).toBeGreaterThan(0);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Collision groups', () => {
|
|
49
|
+
it('should register groups starting from 1 (0 is the reserved main collision group)', () => {
|
|
50
|
+
expect(world.mainCollisionGroup).toBe(0);
|
|
51
|
+
expect(world.registerCollisionGroup()).toBe(1);
|
|
52
|
+
expect(world.registerCollisionGroup()).toBe(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should let a deregistered group id be reused', () => {
|
|
56
|
+
const group = world.registerCollisionGroup();
|
|
57
|
+
world.deregisterCollisionGroup(group);
|
|
58
|
+
expect(world.registerCollisionGroup()).toBe(group);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should throw once all 16 collision groups (0-15) are exhausted', () => {
|
|
62
|
+
// group 0 is implicitly reserved as the main collision group, so 15 more can be registered
|
|
63
|
+
for (let i = 0; i < 15; i++) {
|
|
64
|
+
world.registerCollisionGroup();
|
|
65
|
+
}
|
|
66
|
+
expect(() => world.registerCollisionGroup()).toThrow();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
20
70
|
describe('Rigid bodies', () => {
|
|
21
71
|
|
|
22
72
|
it('should simulate inertial motion of rigid body', () => {
|
package/tsconfig.json
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
"extends": "./../../tsconfig.base.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"baseUrl": "./src/",
|
|
5
|
-
"outDir": "./dist/"
|
|
5
|
+
"outDir": "./dist/",
|
|
6
|
+
"rootDir": "./src/",
|
|
7
|
+
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
|
|
8
|
+
"types": ["jest", "node"]
|
|
6
9
|
},
|
|
7
10
|
"include": ["src/*.ts", "src/**/*.ts"],
|
|
11
|
+
"references": [{ "path": "../core" }]
|
|
8
12
|
}
|