spoint 0.1.78 → 0.1.80
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/SKILL.md +15 -21
- package/apps/environment/index.js +6 -6
- package/apps/world/index.js +65 -70
- package/client/app.js +2 -0
- package/package.json +1 -1
- package/src/apps/AppContext.js +2 -2
- package/src/apps/AppRuntime.js +23 -9
- package/src/physics/GLBLoader.js +220 -31
- package/src/physics/World.js +106 -16
- package/apps/arena/index.js +0 -83
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spoint
|
|
3
|
-
description: "Build multiplayer physics games with the Spawnpoint engine. Use when asked to: create a game, add physics objects, spawn entities, build
|
|
3
|
+
description: "Build multiplayer physics games with the Spawnpoint engine. Use when asked to: create a game, add physics objects, spawn entities, build a map/level, handle player interaction, add weapons, respawn, scoring, create moving platforms, manage world config, load 3D models, add HUD/UI, work with the EventBus, or develop any app inside an apps/ directory."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Spawnpoint App Development Reference
|
|
@@ -26,36 +26,30 @@ export default {
|
|
|
26
26
|
movement: { maxSpeed: 4.0, groundAccel: 10.0, airAccel: 1.0, friction: 6.0, stopSpeed: 2.0, jumpImpulse: 4.0 },
|
|
27
27
|
player: { health: 100, capsuleRadius: 0.4, capsuleHalfHeight: 0.9, modelScale: 1.323, feetOffset: 0.212 },
|
|
28
28
|
scene: { skyColor: 0x87ceeb, fogColor: 0x87ceeb, fogNear: 80, fogFar: 200, sunIntensity: 1.5, sunPosition: [20, 40, 20] },
|
|
29
|
-
entities: [{ id: '
|
|
29
|
+
entities: [{ id: 'environment', model: './apps/game/map.glb', position: [0,0,0], app: 'environment' }],
|
|
30
30
|
spawnPoint: [0, 2, 0]
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
### `apps/
|
|
34
|
+
### `apps/environment/index.js` — static environment with trimesh physics
|
|
35
35
|
```js
|
|
36
|
-
const HALF = 12, WH = 3, WT = 0.5
|
|
37
|
-
const WALLS = [
|
|
38
|
-
{ id:'wn', x:0, y:WH/2, z:-HALF, hx:HALF, hy:WH/2, hz:WT/2 },
|
|
39
|
-
{ id:'ws', x:0, y:WH/2, z: HALF, hx:HALF, hy:WH/2, hz:WT/2 },
|
|
40
|
-
{ id:'we', x: HALF, y:WH/2, z:0, hx:WT/2, hy:WH/2, hz:HALF },
|
|
41
|
-
{ id:'ww', x:-HALF, y:WH/2, z:0, hx:WT/2, hy:WH/2, hz:HALF },
|
|
42
|
-
]
|
|
43
36
|
export default {
|
|
44
37
|
server: {
|
|
45
|
-
setup(ctx) {
|
|
46
|
-
ctx.state.ids = ctx.state.ids || []
|
|
47
|
-
if (ctx.state.ids.length > 0) return // hot-reload guard
|
|
48
|
-
ctx.entity.custom = { mesh:'box', color:0x5a7a4a, sx:HALF*2, sy:0.5, sz:HALF*2 }
|
|
38
|
+
async setup(ctx) {
|
|
49
39
|
ctx.physics.setStatic(true)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
try {
|
|
41
|
+
await ctx.physics.addTrimeshCollider()
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.log(\`Trimesh failed: \${e.message}, using box fallback\`)
|
|
44
|
+
ctx.physics.addBoxCollider([100, 25, 100])
|
|
54
45
|
}
|
|
55
|
-
}
|
|
56
|
-
teardown(ctx) { for (const id of ctx.state.ids||[]) ctx.world.destroy(id); ctx.state.ids = [] }
|
|
46
|
+
}
|
|
57
47
|
},
|
|
58
|
-
client: {
|
|
48
|
+
client: {
|
|
49
|
+
render(ctx) {
|
|
50
|
+
return { model: ctx.entity.model, position: ctx.entity.position, rotation: ctx.entity.rotation }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
59
53
|
}
|
|
60
54
|
```
|
|
61
55
|
|
|
@@ -23,15 +23,15 @@ function discoverModels() {
|
|
|
23
23
|
|
|
24
24
|
export default {
|
|
25
25
|
server: {
|
|
26
|
-
setup(ctx) {
|
|
26
|
+
async setup(ctx) {
|
|
27
27
|
ctx.physics.setStatic(true)
|
|
28
|
-
// Use trimesh collider for environment
|
|
28
|
+
// Use trimesh collider for accurate environment collision
|
|
29
|
+
// Deferred to background so server startup isn't blocked
|
|
29
30
|
try {
|
|
30
|
-
ctx.physics.addTrimeshCollider()
|
|
31
|
+
await ctx.physics.addTrimeshCollider()
|
|
31
32
|
} catch (e) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
ctx.physics.addBoxCollider([50, 0.5, 50])
|
|
33
|
+
console.log(`[Environment] Trimesh collider failed: ${e.message}, using box collider`)
|
|
34
|
+
ctx.physics.addBoxCollider([100, 25, 100])
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
ctx.state.smartObjects = new Map()
|
package/apps/world/index.js
CHANGED
|
@@ -1,70 +1,65 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
port: 3001,
|
|
3
|
-
tickRate: 128,
|
|
4
|
-
gravity: [0, -9.81, 0],
|
|
5
|
-
movement: {
|
|
6
|
-
maxSpeed: 4.0,
|
|
7
|
-
groundAccel: 10.0,
|
|
8
|
-
airAccel: 1.0,
|
|
9
|
-
friction: 6.0,
|
|
10
|
-
stopSpeed: 2.0,
|
|
11
|
-
jumpImpulse: 4.0,
|
|
12
|
-
collisionRestitution: 0.2,
|
|
13
|
-
collisionDamping: 0.25
|
|
14
|
-
},
|
|
15
|
-
player: {
|
|
16
|
-
health: 100,
|
|
17
|
-
capsuleRadius: 0.4,
|
|
18
|
-
capsuleHalfHeight: 0.9,
|
|
19
|
-
crouchHalfHeight: 0.45,
|
|
20
|
-
mass: 120,
|
|
21
|
-
modelScale: 1.323,
|
|
22
|
-
feetOffset: 0.212
|
|
23
|
-
},
|
|
24
|
-
scene: {
|
|
25
|
-
skyColor: 0x87ceeb,
|
|
26
|
-
fogColor: 0x87ceeb,
|
|
27
|
-
fogNear: 80,
|
|
28
|
-
fogFar: 200,
|
|
29
|
-
ambientColor: 0xfff4d6,
|
|
30
|
-
ambientIntensity: 0.3,
|
|
31
|
-
sunColor: 0xffffff,
|
|
32
|
-
sunIntensity: 1.5,
|
|
33
|
-
sunPosition: [21, 50, 20],
|
|
34
|
-
fillColor: 0x4488ff,
|
|
35
|
-
fillIntensity: 0.4,
|
|
36
|
-
fillPosition: [-20, 30, -10],
|
|
37
|
-
shadowMapSize: 1024,
|
|
38
|
-
shadowBias: 0.0038,
|
|
39
|
-
shadowNormalBias: 0.6,
|
|
40
|
-
shadowRadius: 12,
|
|
41
|
-
shadowBlurSamples: 8
|
|
42
|
-
},
|
|
43
|
-
camera: {
|
|
44
|
-
fov: 70,
|
|
45
|
-
shoulderOffset: 0.35,
|
|
46
|
-
headHeight: 0.4,
|
|
47
|
-
zoomStages: [0, 1.5, 3, 5, 8],
|
|
48
|
-
defaultZoomIndex: 2,
|
|
49
|
-
followSpeed: 12.0,
|
|
50
|
-
snapSpeed: 30.0,
|
|
51
|
-
mouseSensitivity: 0.002,
|
|
52
|
-
pitchRange: [-1.4, 1.4]
|
|
53
|
-
},
|
|
54
|
-
animation: {
|
|
55
|
-
mixerTimeScale: 1.3,
|
|
56
|
-
walkTimeScale: 2.0,
|
|
57
|
-
sprintTimeScale: 0.56,
|
|
58
|
-
fadeTime: 0.15
|
|
59
|
-
},
|
|
60
|
-
entities: [
|
|
61
|
-
{ id: 'environment', model: './apps/tps-game/schwust.glb', position: [0,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// { id: 'arena', position: [0, 0, 0], app: 'arena' }
|
|
67
|
-
],
|
|
68
|
-
playerModel: './apps/tps-game/cleetus.vrm',
|
|
69
|
-
spawnPoint: [-35, 3, -65]
|
|
70
|
-
}
|
|
1
|
+
export default {
|
|
2
|
+
port: 3001,
|
|
3
|
+
tickRate: 128,
|
|
4
|
+
gravity: [0, -9.81, 0],
|
|
5
|
+
movement: {
|
|
6
|
+
maxSpeed: 4.0,
|
|
7
|
+
groundAccel: 10.0,
|
|
8
|
+
airAccel: 1.0,
|
|
9
|
+
friction: 6.0,
|
|
10
|
+
stopSpeed: 2.0,
|
|
11
|
+
jumpImpulse: 4.0,
|
|
12
|
+
collisionRestitution: 0.2,
|
|
13
|
+
collisionDamping: 0.25
|
|
14
|
+
},
|
|
15
|
+
player: {
|
|
16
|
+
health: 100,
|
|
17
|
+
capsuleRadius: 0.4,
|
|
18
|
+
capsuleHalfHeight: 0.9,
|
|
19
|
+
crouchHalfHeight: 0.45,
|
|
20
|
+
mass: 120,
|
|
21
|
+
modelScale: 1.323,
|
|
22
|
+
feetOffset: 0.212
|
|
23
|
+
},
|
|
24
|
+
scene: {
|
|
25
|
+
skyColor: 0x87ceeb,
|
|
26
|
+
fogColor: 0x87ceeb,
|
|
27
|
+
fogNear: 80,
|
|
28
|
+
fogFar: 200,
|
|
29
|
+
ambientColor: 0xfff4d6,
|
|
30
|
+
ambientIntensity: 0.3,
|
|
31
|
+
sunColor: 0xffffff,
|
|
32
|
+
sunIntensity: 1.5,
|
|
33
|
+
sunPosition: [21, 50, 20],
|
|
34
|
+
fillColor: 0x4488ff,
|
|
35
|
+
fillIntensity: 0.4,
|
|
36
|
+
fillPosition: [-20, 30, -10],
|
|
37
|
+
shadowMapSize: 1024,
|
|
38
|
+
shadowBias: 0.0038,
|
|
39
|
+
shadowNormalBias: 0.6,
|
|
40
|
+
shadowRadius: 12,
|
|
41
|
+
shadowBlurSamples: 8
|
|
42
|
+
},
|
|
43
|
+
camera: {
|
|
44
|
+
fov: 70,
|
|
45
|
+
shoulderOffset: 0.35,
|
|
46
|
+
headHeight: 0.4,
|
|
47
|
+
zoomStages: [0, 1.5, 3, 5, 8],
|
|
48
|
+
defaultZoomIndex: 2,
|
|
49
|
+
followSpeed: 12.0,
|
|
50
|
+
snapSpeed: 30.0,
|
|
51
|
+
mouseSensitivity: 0.002,
|
|
52
|
+
pitchRange: [-1.4, 1.4]
|
|
53
|
+
},
|
|
54
|
+
animation: {
|
|
55
|
+
mixerTimeScale: 1.3,
|
|
56
|
+
walkTimeScale: 2.0,
|
|
57
|
+
sprintTimeScale: 0.56,
|
|
58
|
+
fadeTime: 0.15
|
|
59
|
+
},
|
|
60
|
+
entities: [
|
|
61
|
+
{ id: 'environment', model: './apps/tps-game/schwust.glb', position: [0, 0, 0], app: 'environment' }
|
|
62
|
+
],
|
|
63
|
+
playerModel: './apps/tps-game/cleetus.vrm',
|
|
64
|
+
spawnPoint: [-30, 7.6, -30]
|
|
65
|
+
}
|
package/client/app.js
CHANGED
|
@@ -5,6 +5,7 @@ THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree
|
|
|
5
5
|
THREE.Mesh.prototype.raycast = acceleratedRaycast
|
|
6
6
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
|
|
7
7
|
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
|
|
8
|
+
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'
|
|
8
9
|
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm'
|
|
9
10
|
import { PhysicsNetworkClient, InputHandler, MSG } from '/src/index.client.js'
|
|
10
11
|
import { createElement, applyDiff } from 'webjsx'
|
|
@@ -675,6 +676,7 @@ const dracoLoader = new DRACOLoader(loadingManager)
|
|
|
675
676
|
dracoLoader.setDecoderPath('/draco/')
|
|
676
677
|
dracoLoader.setWorkerLimit(1)
|
|
677
678
|
gltfLoader.setDRACOLoader(dracoLoader)
|
|
679
|
+
gltfLoader.setMeshoptDecoder(MeshoptDecoder)
|
|
678
680
|
gltfLoader.register((parser) => new VRMLoaderPlugin(parser))
|
|
679
681
|
const playerMeshes = new Map()
|
|
680
682
|
const playerAnimators = new Map()
|
package/package.json
CHANGED
package/src/apps/AppContext.js
CHANGED
|
@@ -69,10 +69,10 @@ export class AppContext {
|
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
addMeshCollider: (m) => { ent.collider = { type: 'mesh', mesh: m } },
|
|
72
|
-
addTrimeshCollider: () => {
|
|
72
|
+
addTrimeshCollider: async () => {
|
|
73
73
|
ent.collider = { type: 'trimesh', model: ent.model }
|
|
74
74
|
if (runtime._physics && ent.model) {
|
|
75
|
-
const bodyId = runtime._physics.
|
|
75
|
+
const bodyId = await runtime._physics.addStaticTrimeshAsync(runtime.resolveAssetPath(ent.model), 0, ent.position)
|
|
76
76
|
ent._physicsBodyId = bodyId
|
|
77
77
|
}
|
|
78
78
|
},
|
package/src/apps/AppRuntime.js
CHANGED
|
@@ -64,25 +64,27 @@ export class AppRuntime {
|
|
|
64
64
|
}
|
|
65
65
|
if (config.autoTrimesh && entity.model && this._physics) {
|
|
66
66
|
entity.collider = { type: 'trimesh', model: entity.model }
|
|
67
|
-
|
|
67
|
+
this._physics.addStaticTrimeshAsync(this.resolveAssetPath(entity.model), 0)
|
|
68
|
+
.then(id => { entity._physicsBodyId = id })
|
|
69
|
+
.catch(e => console.error(`[AppRuntime] Failed to create trimesh for ${entity.model}:`, e.message))
|
|
68
70
|
}
|
|
69
|
-
if (config.app) this._attachApp(entityId, config.app)
|
|
71
|
+
if (config.app) this._attachApp(entityId, config.app).catch(e => console.error(`[AppRuntime] Failed to attach app ${config.app}:`, e.message))
|
|
70
72
|
this._spatialInsert(entity)
|
|
71
73
|
return entity
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
_attachApp(entityId, appName) {
|
|
76
|
+
async _attachApp(entityId, appName) {
|
|
75
77
|
const entity = this.entities.get(entityId), appDef = this._appDefs.get(appName)
|
|
76
78
|
if (!entity || !appDef) return
|
|
77
79
|
const ctx = new AppContext(entity, this)
|
|
78
80
|
this.contexts.set(entityId, ctx); this.apps.set(entityId, appDef)
|
|
79
|
-
this._safeCall(appDef.server || appDef, 'setup', [ctx], `setup(${appName})`)
|
|
81
|
+
await this._safeCall(appDef.server || appDef, 'setup', [ctx], `setup(${appName})`)
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
attachApp(entityId, appName) { this._attachApp(entityId, appName) }
|
|
83
|
-
spawnWithApp(id, cfg = {}, app) { return this.spawnEntity(id, { ...cfg, app }) }
|
|
84
|
-
attachAppToEntity(eid, app, cfg = {}) { const e = this.getEntity(eid); if (!e) return false; e._config = cfg; this._attachApp(eid, app); return true }
|
|
85
|
-
reattachAppToEntity(eid, app) { this.detachApp(eid); this._attachApp(eid, app) }
|
|
84
|
+
async attachApp(entityId, appName) { await this._attachApp(entityId, appName) }
|
|
85
|
+
async spawnWithApp(id, cfg = {}, app) { return await this.spawnEntity(id, { ...cfg, app }) }
|
|
86
|
+
async attachAppToEntity(eid, app, cfg = {}) { const e = this.getEntity(eid); if (!e) return false; e._config = cfg; await this._attachApp(eid, app); return true }
|
|
87
|
+
async reattachAppToEntity(eid, app) { this.detachApp(eid); await this._attachApp(eid, app) }
|
|
86
88
|
getEntityWithApp(eid) { const e = this.entities.get(eid); return { entity: e, appName: e?._appName, hasApp: !!e?._appName } }
|
|
87
89
|
|
|
88
90
|
detachApp(entityId) {
|
|
@@ -234,5 +236,17 @@ export class AppRuntime {
|
|
|
234
236
|
relevantEntities(position, radius) { if (!this._stageLoader) return Array.from(this.entities.keys()); return this._stageLoader.getRelevantEntities(position, radius) }
|
|
235
237
|
|
|
236
238
|
_log(type, data, meta = {}) { if (this._eventLog) this._eventLog.record(type, data, { ...meta, tick: this.currentTick }) }
|
|
237
|
-
_safeCall(o, m, a, l) {
|
|
239
|
+
_safeCall(o, m, a, l) {
|
|
240
|
+
if (!o?.[m]) return Promise.resolve()
|
|
241
|
+
try {
|
|
242
|
+
const result = o[m](...a)
|
|
243
|
+
if (result && typeof result.catch === 'function') {
|
|
244
|
+
return result.catch(e => console.error(`[AppRuntime] ${l}: ${e.message}\n ${e.stack?.split('\n').slice(1, 3).join('\n ') || ''}`))
|
|
245
|
+
}
|
|
246
|
+
return Promise.resolve()
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.error(`[AppRuntime] ${l}: ${e.message}\n ${e.stack?.split('\n').slice(1, 3).join('\n ') || ''}`)
|
|
249
|
+
return Promise.reject(e)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
238
252
|
}
|
package/src/physics/GLBLoader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
3
|
let _dracoDecoderPromise = null
|
|
4
|
+
let _meshoptDecoderPromise = null
|
|
4
5
|
|
|
5
6
|
async function getDracoDecoder() {
|
|
6
7
|
if (!_dracoDecoderPromise) {
|
|
@@ -14,6 +15,19 @@ async function getDracoDecoder() {
|
|
|
14
15
|
return _dracoDecoderPromise
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
async function getMeshoptDecoder() {
|
|
19
|
+
if (!_meshoptDecoderPromise) {
|
|
20
|
+
try {
|
|
21
|
+
const meshopt = await import('meshoptimizer')
|
|
22
|
+
_meshoptDecoderPromise = meshopt.MeshoptDecoder
|
|
23
|
+
await _meshoptDecoderPromise.ready
|
|
24
|
+
} catch(e) {
|
|
25
|
+
throw new Error(`Failed to load Meshopt decoder: ${e.message}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return _meshoptDecoderPromise
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
/**
|
|
18
32
|
* Extract mesh from GLB file for physics collider creation.
|
|
19
33
|
* Supports both standard and Draco-compressed meshes.
|
|
@@ -42,6 +56,13 @@ export function extractMeshFromGLB(filepath, meshIndex = 0) {
|
|
|
42
56
|
throw new Error('Draco-compressed mesh detected. Use extractMeshFromGLBAsync() instead.')
|
|
43
57
|
}
|
|
44
58
|
|
|
59
|
+
// Check for meshopt compression (not supported)
|
|
60
|
+
const hasMeshopt = json.bufferViews?.some(bv => bv.extensions?.EXT_meshopt_compression) ||
|
|
61
|
+
json.meshes.some(m => m.primitives.some(p => p.extensions?.EXT_meshopt_compression))
|
|
62
|
+
if (hasMeshopt) {
|
|
63
|
+
throw new Error('Meshopt-compressed mesh detected. Decompress with gltfpack first: gltfpack -i input.glb -o output.glb -noq')
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
// Standard uncompressed GLB mesh extraction
|
|
46
67
|
const posAcc = json.accessors[prim.attributes.POSITION]
|
|
47
68
|
const posView = json.bufferViews[posAcc.bufferView]
|
|
@@ -61,13 +82,24 @@ export function extractMeshFromGLB(filepath, meshIndex = 0) {
|
|
|
61
82
|
}
|
|
62
83
|
}
|
|
63
84
|
|
|
64
|
-
|
|
85
|
+
const result = {
|
|
65
86
|
vertices,
|
|
66
87
|
indices,
|
|
67
88
|
vertexCount: posAcc.count,
|
|
68
89
|
triangleCount: indices ? indices.length / 3 : 0,
|
|
69
90
|
name: mesh.name
|
|
70
91
|
}
|
|
92
|
+
|
|
93
|
+
const node = json.nodes?.find(n => n.mesh === meshIndex)
|
|
94
|
+
if (node) {
|
|
95
|
+
result.nodeTransform = {
|
|
96
|
+
scale: node.scale ? [...node.scale] : null,
|
|
97
|
+
rotation: node.rotation ? [...node.rotation] : null,
|
|
98
|
+
translation: node.translation ? [...node.translation] : null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result
|
|
71
103
|
}
|
|
72
104
|
|
|
73
105
|
/**
|
|
@@ -92,12 +124,29 @@ export async function extractMeshFromGLBAsync(filepath, meshIndex = 0) {
|
|
|
92
124
|
|
|
93
125
|
const prim = mesh.primitives[0]
|
|
94
126
|
|
|
95
|
-
|
|
127
|
+
let result
|
|
128
|
+
|
|
96
129
|
if (prim.extensions?.KHR_draco_mesh_compression) {
|
|
97
|
-
|
|
130
|
+
result = await decompressDracoMesh(buf, json, prim, binOffset, mesh.name)
|
|
131
|
+
} else if (json.bufferViews?.some(bv => bv.extensions?.EXT_meshopt_compression)) {
|
|
132
|
+
result = await extractMeshWithMeshopt(buf, json, prim, binOffset, mesh.name)
|
|
133
|
+
} else {
|
|
134
|
+
result = extractStandardMesh(buf, json, prim, binOffset, mesh.name)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const node = json.nodes?.find(n => n.mesh === meshIndex)
|
|
138
|
+
if (node) {
|
|
139
|
+
result.nodeTransform = {
|
|
140
|
+
scale: node.scale ? [...node.scale] : null,
|
|
141
|
+
rotation: node.rotation ? [...node.rotation] : null,
|
|
142
|
+
translation: node.translation ? [...node.translation] : null
|
|
143
|
+
}
|
|
98
144
|
}
|
|
99
145
|
|
|
100
|
-
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractStandardMesh(buf, json, prim, binOffset, meshName) {
|
|
101
150
|
const posAcc = json.accessors[prim.attributes.POSITION]
|
|
102
151
|
const posView = json.bufferViews[posAcc.bufferView]
|
|
103
152
|
const posOff = binOffset + (posView.byteOffset || 0) + (posAcc.byteOffset || 0)
|
|
@@ -121,7 +170,40 @@ export async function extractMeshFromGLBAsync(filepath, meshIndex = 0) {
|
|
|
121
170
|
indices,
|
|
122
171
|
vertexCount: posAcc.count,
|
|
123
172
|
triangleCount: indices ? indices.length / 3 : 0,
|
|
124
|
-
name:
|
|
173
|
+
name: meshName
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function applyNodeTransform(vertices, node) {
|
|
178
|
+
const scale = node.scale ? [node.scale[0], node.scale[1], node.scale[2]] : [1, 1, 1]
|
|
179
|
+
const translation = node.translation ? [node.translation[0], node.translation[1], node.translation[2]] : [0, 0, 0]
|
|
180
|
+
|
|
181
|
+
if (node.rotation) {
|
|
182
|
+
const [qx, qy, qz, qw] = node.rotation
|
|
183
|
+
for (let i = 0; i < vertices.length / 3; i++) {
|
|
184
|
+
let x = vertices[i * 3] * scale[0]
|
|
185
|
+
let y = vertices[i * 3 + 1] * scale[1]
|
|
186
|
+
let z = vertices[i * 3 + 2] * scale[2]
|
|
187
|
+
|
|
188
|
+
const ix = qw * x + qy * z - qz * y
|
|
189
|
+
const iy = qw * y + qz * x - qx * z
|
|
190
|
+
const iz = qw * z + qx * y - qy * x
|
|
191
|
+
const iw = -qx * x - qy * y - qz * z
|
|
192
|
+
|
|
193
|
+
x = ix * qw - iw * qx - iy * qz + iz * qy
|
|
194
|
+
y = iy * qw - iw * qy - iz * qx + ix * qz
|
|
195
|
+
z = iz * qw - iw * qz - ix * qy + iy * qx
|
|
196
|
+
|
|
197
|
+
vertices[i * 3] = x + translation[0]
|
|
198
|
+
vertices[i * 3 + 1] = y + translation[1]
|
|
199
|
+
vertices[i * 3 + 2] = z + translation[2]
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
for (let i = 0; i < vertices.length / 3; i++) {
|
|
203
|
+
vertices[i * 3] = vertices[i * 3] * scale[0] + translation[0]
|
|
204
|
+
vertices[i * 3 + 1] = vertices[i * 3 + 1] * scale[1] + translation[1]
|
|
205
|
+
vertices[i * 3 + 2] = vertices[i * 3 + 2] * scale[2] + translation[2]
|
|
206
|
+
}
|
|
125
207
|
}
|
|
126
208
|
}
|
|
127
209
|
|
|
@@ -135,76 +217,183 @@ async function decompressDracoMesh(buf, json, prim, binOffset, meshName) {
|
|
|
135
217
|
const length = bufView.byteLength
|
|
136
218
|
const dracoData = buf.slice(offset, offset + length)
|
|
137
219
|
|
|
138
|
-
// Create decoder and buffer
|
|
139
220
|
const d = new decoder.Decoder()
|
|
140
221
|
const db = new decoder.DecoderBuffer()
|
|
222
|
+
const decodedGeom = new decoder.Mesh()
|
|
141
223
|
|
|
142
224
|
try {
|
|
143
|
-
// Initialize buffer
|
|
144
225
|
const dracoArray = new Uint8Array(dracoData)
|
|
145
226
|
db.Init(dracoArray, dracoArray.length)
|
|
146
227
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
throw new Error('Draco decompression failed: empty result')
|
|
228
|
+
const status = d.DecodeBufferToMesh(db, decodedGeom)
|
|
229
|
+
if (!status.ok()) {
|
|
230
|
+
throw new Error(`Draco decompression failed: ${status.error_msg()}`)
|
|
151
231
|
}
|
|
152
232
|
|
|
153
|
-
|
|
154
|
-
const posAttrId = d.GetAttributeIdByName(decodedGeom, 'POSITION')
|
|
233
|
+
const posAttrId = d.GetAttributeId(decodedGeom, decoder.POSITION)
|
|
155
234
|
if (posAttrId < 0) {
|
|
156
235
|
throw new Error('No POSITION attribute in decompressed mesh')
|
|
157
236
|
}
|
|
158
237
|
|
|
159
238
|
const posAttr = d.GetAttribute(decodedGeom, posAttrId)
|
|
160
|
-
const
|
|
161
|
-
const
|
|
239
|
+
const numPoints = decodedGeom.num_points()
|
|
240
|
+
const posData = new decoder.DracoFloat32Array()
|
|
241
|
+
d.GetAttributeFloatForAllPoints(decodedGeom, posAttr, posData)
|
|
242
|
+
|
|
243
|
+
const vertices = new Float32Array(numPoints * 3)
|
|
244
|
+
for (let i = 0; i < numPoints * 3; i++) {
|
|
245
|
+
vertices[i] = posData.GetValue(i)
|
|
246
|
+
}
|
|
162
247
|
|
|
163
|
-
// Get indices if available
|
|
164
248
|
let indices = null
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
indices = new Uint32Array(
|
|
249
|
+
const numFaces = decodedGeom.num_faces()
|
|
250
|
+
if (numFaces > 0) {
|
|
251
|
+
indices = new Uint32Array(numFaces * 3)
|
|
252
|
+
const faceIndices = new decoder.DracoUInt32Array()
|
|
253
|
+
for (let i = 0; i < numFaces; i++) {
|
|
254
|
+
d.GetFaceFromMesh(decodedGeom, i, faceIndices)
|
|
255
|
+
indices[i * 3] = faceIndices.GetValue(0)
|
|
256
|
+
indices[i * 3 + 1] = faceIndices.GetValue(1)
|
|
257
|
+
indices[i * 3 + 2] = faceIndices.GetValue(2)
|
|
258
|
+
}
|
|
259
|
+
decoder.destroy(faceIndices)
|
|
168
260
|
}
|
|
169
261
|
|
|
170
|
-
decoder.destroy(
|
|
262
|
+
decoder.destroy(posData)
|
|
263
|
+
decoder.destroy(status)
|
|
171
264
|
|
|
172
265
|
return {
|
|
173
266
|
vertices,
|
|
174
267
|
indices,
|
|
175
|
-
vertexCount:
|
|
176
|
-
triangleCount:
|
|
268
|
+
vertexCount: numPoints,
|
|
269
|
+
triangleCount: numFaces,
|
|
177
270
|
name: meshName
|
|
178
271
|
}
|
|
179
272
|
} finally {
|
|
273
|
+
decoder.destroy(decodedGeom)
|
|
180
274
|
decoder.destroy(d)
|
|
181
275
|
decoder.destroy(db)
|
|
182
276
|
}
|
|
183
277
|
}
|
|
184
278
|
|
|
279
|
+
async function extractMeshWithMeshopt(buf, json, prim, binOffset, meshName) {
|
|
280
|
+
const decoder = await getMeshoptDecoder()
|
|
281
|
+
|
|
282
|
+
const posAcc = json.accessors[prim.attributes.POSITION]
|
|
283
|
+
const posView = json.bufferViews[posAcc.bufferView]
|
|
284
|
+
const posExt = posView.extensions?.EXT_meshopt_compression
|
|
285
|
+
|
|
286
|
+
if (!posExt) {
|
|
287
|
+
throw new Error('Position buffer view has no EXT_meshopt_compression extension')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const posSrcOff = binOffset + (posExt.byteOffset || 0)
|
|
291
|
+
const posSrcLen = posExt.byteLength
|
|
292
|
+
const posSrc = new Uint8Array(buf.buffer.slice(posSrcOff, posSrcOff + posSrcLen))
|
|
293
|
+
|
|
294
|
+
const numVertices = posExt.count
|
|
295
|
+
const stride = posExt.byteStride || 12
|
|
296
|
+
const posDst = new Uint8Array(numVertices * stride)
|
|
297
|
+
|
|
298
|
+
const mode = posExt.mode || 'ATTRIBUTES'
|
|
299
|
+
const filter = posExt.filter || 'NONE'
|
|
300
|
+
decoder.decodeGltfBuffer(posDst, numVertices, stride, posSrc, mode, filter)
|
|
301
|
+
|
|
302
|
+
const vertices = new Float32Array(numVertices * 3)
|
|
303
|
+
|
|
304
|
+
// Handle normalized INT16 positions (common in meshopt-compressed models)
|
|
305
|
+
// Normalized INT16: float = raw / 32767.0
|
|
306
|
+
if (stride === 8 && posAcc.normalized && posAcc.componentType === 5122) {
|
|
307
|
+
const raw = new Int16Array(posDst.buffer)
|
|
308
|
+
for (let i = 0; i < numVertices; i++) {
|
|
309
|
+
vertices[i * 3] = raw[i * 4] / 32767.0
|
|
310
|
+
vertices[i * 3 + 1] = raw[i * 4 + 1] / 32767.0
|
|
311
|
+
vertices[i * 3 + 2] = raw[i * 4 + 2] / 32767.0
|
|
312
|
+
}
|
|
313
|
+
} else if (stride === 12) {
|
|
314
|
+
vertices.set(new Float32Array(posDst.buffer))
|
|
315
|
+
} else {
|
|
316
|
+
const floats = new Float32Array(posDst.buffer)
|
|
317
|
+
for (let i = 0; i < numVertices; i++) {
|
|
318
|
+
vertices[i * 3] = floats[i * (stride / 4)]
|
|
319
|
+
vertices[i * 3 + 1] = floats[i * (stride / 4) + 1]
|
|
320
|
+
vertices[i * 3 + 2] = floats[i * (stride / 4) + 2]
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let indices = null
|
|
325
|
+
if (prim.indices !== undefined) {
|
|
326
|
+
const idxAcc = json.accessors[prim.indices]
|
|
327
|
+
const idxView = json.bufferViews[idxAcc.bufferView]
|
|
328
|
+
const idxExt = idxView.extensions?.EXT_meshopt_compression
|
|
329
|
+
|
|
330
|
+
if (idxExt) {
|
|
331
|
+
const idxSrcOff = binOffset + (idxExt.byteOffset || 0)
|
|
332
|
+
const idxSrcLen = idxExt.byteLength
|
|
333
|
+
const idxSrc = new Uint8Array(buf.buffer.slice(idxSrcOff, idxSrcOff + idxSrcLen))
|
|
334
|
+
|
|
335
|
+
const idxStride = idxExt.byteStride || 2
|
|
336
|
+
const numIndices = idxExt.count
|
|
337
|
+
const idxDst = new Uint8Array(numIndices * idxStride)
|
|
338
|
+
|
|
339
|
+
const idxMode = idxExt.mode || 'TRIANGLES'
|
|
340
|
+
const idxFilter = idxExt.filter || 'NONE'
|
|
341
|
+
decoder.decodeGltfBuffer(idxDst, numIndices, idxStride, idxSrc, idxMode, idxFilter)
|
|
342
|
+
|
|
343
|
+
if (idxAcc.componentType === 5123) {
|
|
344
|
+
indices = new Uint32Array(new Uint16Array(idxDst.buffer))
|
|
345
|
+
} else {
|
|
346
|
+
indices = new Uint32Array(idxDst.buffer)
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
const idxOff = binOffset + (idxView.byteOffset || 0) + (idxAcc.byteOffset || 0)
|
|
350
|
+
if (idxAcc.componentType === 5123) {
|
|
351
|
+
indices = new Uint32Array(new Uint16Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 2)))
|
|
352
|
+
} else {
|
|
353
|
+
indices = new Uint32Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 4))
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
vertices,
|
|
360
|
+
indices,
|
|
361
|
+
vertexCount: numVertices,
|
|
362
|
+
triangleCount: indices ? indices.length / 3 : 0,
|
|
363
|
+
name: meshName
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
185
367
|
/**
|
|
186
368
|
* Check if a GLB file has Draco-compressed meshes without attempting extraction.
|
|
187
369
|
* Useful for validation and error reporting.
|
|
188
370
|
*
|
|
189
371
|
* @param {string} filepath - Path to GLB file
|
|
190
|
-
* @returns {Object} {hasDraco: boolean, meshes: Array<{name, hasDraco}>}
|
|
372
|
+
* @returns {Object} {hasDraco: boolean, hasMeshopt: boolean, meshes: Array<{name, hasDraco, hasMeshopt}>}
|
|
191
373
|
*/
|
|
192
374
|
export function detectDracoInGLB(filepath) {
|
|
193
375
|
try {
|
|
194
376
|
const buf = readFileSync(filepath)
|
|
195
|
-
if (buf.toString('ascii', 0, 4) !== 'glTF') return { hasDraco: false, meshes: [] }
|
|
377
|
+
if (buf.toString('ascii', 0, 4) !== 'glTF') return { hasDraco: false, hasMeshopt: false, meshes: [] }
|
|
196
378
|
|
|
197
379
|
const jsonLen = buf.readUInt32LE(12)
|
|
198
380
|
const json = JSON.parse(buf.toString('utf-8', 20, 20 + jsonLen))
|
|
199
381
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
hasDraco: mesh.primitives?.some(p => p.extensions?.KHR_draco_mesh_compression) || false
|
|
203
|
-
}))
|
|
382
|
+
const bufferViewHasMeshopt = (bv) => bv.extensions?.EXT_meshopt_compression
|
|
383
|
+
const hasMeshoptGlobally = (json.bufferViews || []).some(bufferViewHasMeshopt)
|
|
204
384
|
|
|
205
|
-
const
|
|
206
|
-
|
|
385
|
+
const meshes = (json.meshes || []).map(mesh => {
|
|
386
|
+
const hasDraco = mesh.primitives?.some(p => p.extensions?.KHR_draco_mesh_compression) || false
|
|
387
|
+
const hasMeshopt = hasMeshoptGlobally || mesh.primitives?.some(p => p.extensions?.EXT_meshopt_compression) || false
|
|
388
|
+
return { name: mesh.name || 'unnamed', hasDraco, hasMeshopt }
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
hasDraco: meshes.some(m => m.hasDraco),
|
|
393
|
+
hasMeshopt: meshes.some(m => m.hasMeshopt),
|
|
394
|
+
meshes
|
|
395
|
+
}
|
|
207
396
|
} catch (e) {
|
|
208
|
-
return { hasDraco: false, meshes: [], error: e.message }
|
|
397
|
+
return { hasDraco: false, hasMeshopt: false, meshes: [], error: e.message }
|
|
209
398
|
}
|
|
210
399
|
}
|
package/src/physics/World.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import initJolt from 'jolt-physics/wasm-compat'
|
|
2
|
-
import { extractMeshFromGLB } from './GLBLoader.js'
|
|
1
|
+
import initJolt from 'jolt-physics/wasm-compat'
|
|
2
|
+
import { extractMeshFromGLB, extractMeshFromGLBAsync } from './GLBLoader.js'
|
|
3
3
|
const LAYER_STATIC = 0, LAYER_DYNAMIC = 1, NUM_LAYERS = 2
|
|
4
4
|
let joltInstance = null
|
|
5
5
|
async function getJolt() { if (!joltInstance) joltInstance = await initJolt(); return joltInstance }
|
|
@@ -71,20 +71,110 @@ export class PhysicsWorld {
|
|
|
71
71
|
layer = motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC
|
|
72
72
|
return this._addBody(shape, position, mt, layer, { ...opts, meta: { type: motionType, shape: shapeType } })
|
|
73
73
|
}
|
|
74
|
-
addStaticTrimesh(glbPath, meshIndex = 0) {
|
|
75
|
-
const J = this.Jolt
|
|
76
|
-
const mesh = extractMeshFromGLB(glbPath, meshIndex)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
addStaticTrimesh(glbPath, meshIndex = 0) {
|
|
75
|
+
const J = this.Jolt
|
|
76
|
+
const mesh = extractMeshFromGLB(glbPath, meshIndex)
|
|
77
|
+
|
|
78
|
+
// Apply node transform if present (scale, rotation, translation)
|
|
79
|
+
let vertices = mesh.vertices
|
|
80
|
+
const nodeT = mesh.nodeTransform
|
|
81
|
+
if (nodeT) {
|
|
82
|
+
const numVerts = mesh.vertexCount
|
|
83
|
+
vertices = new Float32Array(numVerts * 3)
|
|
84
|
+
|
|
85
|
+
const scale = nodeT.scale || [1, 1, 1]
|
|
86
|
+
const translation = nodeT.translation || [0, 0, 0]
|
|
87
|
+
const rotation = nodeT.rotation
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < numVerts; i++) {
|
|
90
|
+
let x = mesh.vertices[i * 3] * scale[0]
|
|
91
|
+
let y = mesh.vertices[i * 3 + 1] * scale[1]
|
|
92
|
+
let z = mesh.vertices[i * 3 + 2] * scale[2]
|
|
93
|
+
|
|
94
|
+
if (rotation) {
|
|
95
|
+
const [qx, qy, qz, qw] = rotation
|
|
96
|
+
const ix = qw * x + qy * z - qz * y
|
|
97
|
+
const iy = qw * y + qz * x - qx * z
|
|
98
|
+
const iz = qw * z + qx * y - qy * x
|
|
99
|
+
const iw = -qx * x - qy * y - qz * z
|
|
100
|
+
x = ix * qw - iw * qx - iy * qz + iz * qy
|
|
101
|
+
y = iy * qw - iw * qy - iz * qx + ix * qz
|
|
102
|
+
z = iz * qw - iw * qz - ix * qy + iy * qx
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
vertices[i * 3] = x + translation[0]
|
|
106
|
+
vertices[i * 3 + 1] = y + translation[1]
|
|
107
|
+
vertices[i * 3 + 2] = z + translation[2]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const triangles = new J.TriangleList(); triangles.resize(mesh.triangleCount)
|
|
112
|
+
for (let t = 0; t < mesh.triangleCount; t++) {
|
|
113
|
+
const tri = triangles.at(t)
|
|
114
|
+
for (let v = 0; v < 3; v++) {
|
|
115
|
+
const idx = mesh.indices[t * 3 + v]
|
|
116
|
+
tri.set_mV(v, new J.Float3(vertices[idx * 3], vertices[idx * 3 + 1], vertices[idx * 3 + 2]))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const shape = new J.MeshShapeSettings(triangles).Create().Get()
|
|
120
|
+
return this._addBody(shape, [0, 0, 0], J.EMotionType_Static, LAYER_STATIC, { meta: { type: 'static', shape: 'trimesh', mesh: mesh.name, triangles: mesh.triangleCount } })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
addStaticTrimeshAsync(glbPath, meshIndex = 0, position = [0, 0, 0]) {
|
|
124
|
+
return new Promise(async (resolve, reject) => {
|
|
125
|
+
try {
|
|
126
|
+
const J = this.Jolt
|
|
127
|
+
const mesh = await extractMeshFromGLBAsync(glbPath, meshIndex)
|
|
128
|
+
|
|
129
|
+
// Apply node transform if present (scale, rotation, translation)
|
|
130
|
+
let vertices = mesh.vertices
|
|
131
|
+
const nodeT = mesh.nodeTransform
|
|
132
|
+
if (nodeT) {
|
|
133
|
+
const numVerts = mesh.vertexCount
|
|
134
|
+
vertices = new Float32Array(numVerts * 3)
|
|
135
|
+
|
|
136
|
+
const scale = nodeT.scale || [1, 1, 1]
|
|
137
|
+
const translation = nodeT.translation || [0, 0, 0]
|
|
138
|
+
const rotation = nodeT.rotation
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < numVerts; i++) {
|
|
141
|
+
let x = mesh.vertices[i * 3] * scale[0]
|
|
142
|
+
let y = mesh.vertices[i * 3 + 1] * scale[1]
|
|
143
|
+
let z = mesh.vertices[i * 3 + 2] * scale[2]
|
|
144
|
+
|
|
145
|
+
if (rotation) {
|
|
146
|
+
const [qx, qy, qz, qw] = rotation
|
|
147
|
+
const ix = qw * x + qy * z - qz * y
|
|
148
|
+
const iy = qw * y + qz * x - qx * z
|
|
149
|
+
const iz = qw * z + qx * y - qy * x
|
|
150
|
+
const iw = -qx * x - qy * y - qz * z
|
|
151
|
+
x = ix * qw - iw * qx - iy * qz + iz * qy
|
|
152
|
+
y = iy * qw - iw * qy - iz * qx + ix * qz
|
|
153
|
+
z = iz * qw - iw * qz - ix * qy + iy * qx
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
vertices[i * 3] = x + translation[0]
|
|
157
|
+
vertices[i * 3 + 1] = y + translation[1]
|
|
158
|
+
vertices[i * 3 + 2] = z + translation[2]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const triangles = new J.TriangleList(); triangles.resize(mesh.triangleCount)
|
|
163
|
+
for (let t = 0; t < mesh.triangleCount; t++) {
|
|
164
|
+
const tri = triangles.at(t)
|
|
165
|
+
for (let v = 0; v < 3; v++) {
|
|
166
|
+
const idx = mesh.indices[t * 3 + v]
|
|
167
|
+
tri.set_mV(v, new J.Float3(vertices[idx * 3], vertices[idx * 3 + 1], vertices[idx * 3 + 2]))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const shape = new J.MeshShapeSettings(triangles).Create().Get()
|
|
171
|
+
const id = this._addBody(shape, position, J.EMotionType_Static, LAYER_STATIC, { meta: { type: 'static', shape: 'trimesh', mesh: mesh.name, triangles: mesh.triangleCount } })
|
|
172
|
+
resolve(id)
|
|
173
|
+
} catch (e) {
|
|
174
|
+
reject(e)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
88
178
|
addPlayerCharacter(radius, halfHeight, position, mass) {
|
|
89
179
|
const J = this.Jolt
|
|
90
180
|
const cvs = new J.CharacterVirtualSettings()
|
package/apps/arena/index.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
const ASSET_BASE = 'https://raw.githubusercontent.com/anEntrypoint/assets/main'
|
|
2
|
-
|
|
3
|
-
const JUNK_MODELS = [
|
|
4
|
-
`${ASSET_BASE}/dumpster_b076662a_v1.glb`,
|
|
5
|
-
`${ASSET_BASE}/garbage_can_6b3d052b_v1.glb`,
|
|
6
|
-
`${ASSET_BASE}/fire_hydrant_ba0175c1_v1.glb`,
|
|
7
|
-
`${ASSET_BASE}/crushed_oil_barrel_e450f43f_v1.glb`,
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
const HALF = 12
|
|
11
|
-
const WALL_H = 3
|
|
12
|
-
const WALL_T = 0.5
|
|
13
|
-
|
|
14
|
-
const WALLS = [
|
|
15
|
-
{ id: 'arena-wall-n', x: 0, y: WALL_H / 2, z: -HALF, hx: HALF, hy: WALL_H / 2, hz: WALL_T / 2 },
|
|
16
|
-
{ id: 'arena-wall-s', x: 0, y: WALL_H / 2, z: HALF, hx: HALF, hy: WALL_H / 2, hz: WALL_T / 2 },
|
|
17
|
-
{ id: 'arena-wall-e', x: HALF, y: WALL_H / 2, z: 0, hx: WALL_T / 2, hy: WALL_H / 2, hz: HALF },
|
|
18
|
-
{ id: 'arena-wall-w', x: -HALF, y: WALL_H / 2, z: 0, hx: WALL_T / 2, hy: WALL_H / 2, hz: HALF },
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
const PROPS = [
|
|
22
|
-
{ model: 0, x: -6, z: -6, rot: 0.5 },
|
|
23
|
-
{ model: 0, x: 7, z: 5, rot: 2.1 },
|
|
24
|
-
{ model: 1, x: -8, z: 3, rot: 1.0 },
|
|
25
|
-
{ model: 1, x: 5, z: -7, rot: 3.2 },
|
|
26
|
-
{ model: 1, x: 3, z: 8, rot: 0.3 },
|
|
27
|
-
{ model: 2, x: -4, z: -9, rot: 1.5 },
|
|
28
|
-
{ model: 2, x: 9, z: -3, rot: 4.0 },
|
|
29
|
-
{ model: 3, x: -7, z: 7, rot: 0.8 },
|
|
30
|
-
{ model: 3, x: 6, z: 2, rot: 2.7 },
|
|
31
|
-
{ model: 3, x: -3, z: -5, rot: 1.2 },
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
export default {
|
|
35
|
-
server: {
|
|
36
|
-
setup(ctx) {
|
|
37
|
-
ctx.state.ids = ctx.state.ids || []
|
|
38
|
-
if (ctx.state.ids.length > 0) return
|
|
39
|
-
|
|
40
|
-
// Ground (this entity itself)
|
|
41
|
-
ctx.entity.custom = { mesh: 'box', color: 0x5a7a4a, roughness: 1, sx: HALF * 2, sy: 0.5, sz: HALF * 2 }
|
|
42
|
-
ctx.physics.setStatic(true)
|
|
43
|
-
ctx.physics.addBoxCollider([HALF, 0.25, HALF])
|
|
44
|
-
|
|
45
|
-
// Walls - each gets box-static app which adds its own collider
|
|
46
|
-
for (const w of WALLS) {
|
|
47
|
-
const e = ctx.world.spawn(w.id, {
|
|
48
|
-
position: [w.x, w.y, w.z],
|
|
49
|
-
app: 'box-static',
|
|
50
|
-
config: { hx: w.hx, hy: w.hy, hz: w.hz, color: 0x7a6a5a, roughness: 0.9 }
|
|
51
|
-
})
|
|
52
|
-
if (e) ctx.state.ids.push(w.id)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Props from remote asset repo (static, convex hull from model)
|
|
56
|
-
for (let i = 0; i < PROPS.length; i++) {
|
|
57
|
-
const p = PROPS[i]
|
|
58
|
-
const id = `arena-prop-${i}`
|
|
59
|
-
const a = p.rot / 2
|
|
60
|
-
const e = ctx.world.spawn(id, {
|
|
61
|
-
model: JUNK_MODELS[p.model],
|
|
62
|
-
position: [p.x, 0, p.z],
|
|
63
|
-
rotation: [0, Math.sin(a), 0, Math.cos(a)],
|
|
64
|
-
app: 'prop-static'
|
|
65
|
-
})
|
|
66
|
-
if (e) ctx.state.ids.push(id)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
ctx.debug.log(`[arena] setup: ground + ${WALLS.length} walls + ${PROPS.length} props`)
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
teardown(ctx) {
|
|
73
|
-
for (const id of ctx.state.ids || []) ctx.world.destroy(id)
|
|
74
|
-
ctx.state.ids = []
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
client: {
|
|
79
|
-
render(ctx) {
|
|
80
|
-
return { position: ctx.entity.position, rotation: ctx.entity.rotation, custom: ctx.entity.custom }
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|