spoint 0.1.76 → 0.1.78

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.
@@ -25,10 +25,12 @@ export default {
25
25
  server: {
26
26
  setup(ctx) {
27
27
  ctx.physics.setStatic(true)
28
+ // Use trimesh collider for environment - handles both compressed and uncompressed
28
29
  try {
29
30
  ctx.physics.addTrimeshCollider()
30
31
  } catch (e) {
31
- ctx.debug.log(`[Environment] Trimesh collider failed (likely Draco compression), using fallback box collider: ${e.message}`)
32
+ // Fallback if trimesh extraction fails (e.g., unsupported model format)
33
+ console.log(`[Environment] Trimesh collider failed: ${e.message}`)
32
34
  ctx.physics.addBoxCollider([50, 0.5, 50])
33
35
  }
34
36
 
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -41,8 +41,12 @@
41
41
  "node": ">=18.0.0"
42
42
  },
43
43
  "dependencies": {
44
+ "@gltf-transform/extensions": "^4.3.0",
44
45
  "d3-octree": "^1.1.0",
46
+ "draco3d": "^1.5.7",
47
+ "draco3dgltf": "^1.5.7",
45
48
  "jolt-physics": "^0.29.0",
49
+ "meshoptimizer": "^1.0.1",
46
50
  "webjsx": "^0.0.73",
47
51
  "ws": "^8.18.0"
48
52
  },
@@ -51,6 +55,9 @@
51
55
  "@fails-components/webtransport-transport-http3-quiche": "^1.5.3"
52
56
  },
53
57
  "devDependencies": {
54
- "playwright": "^1.58.1"
58
+ "@gltf-transform/cli": "^4.3.0",
59
+ "@gltf-transform/core": "^4.3.0",
60
+ "playwright": "^1.58.1",
61
+ "three": "^0.183.1"
55
62
  }
56
63
  }
@@ -1,5 +1,5 @@
1
1
  import { CliDebugger } from '../debug/CliDebugger.js'
2
- import { extractMeshFromGLB } from '../physics/GLBLoader.js'
2
+ import { extractMeshFromGLB, extractMeshFromGLBAsync } from '../physics/GLBLoader.js'
3
3
 
4
4
  export class AppContext {
5
5
  constructor(entity, runtime) {
@@ -95,7 +95,12 @@ export class AppContext {
95
95
  }
96
96
  } catch (err) {
97
97
  if (err.message.includes('Draco-compressed')) {
98
- runtime._debug?.warn(`[physics] ${err.message}`)
98
+ runtime._debug?.warn(`[physics] Draco mesh detected - use addTrimeshCollider() for physics or box/sphere/capsule for trigger`)
99
+ if (runtime._physics) {
100
+ const mt = ent.bodyType === 'dynamic' ? 'dynamic' : ent.bodyType === 'kinematic' ? 'kinematic' : 'static'
101
+ ent.collider = { type: 'box', size: [0.5, 0.5, 0.5] }
102
+ ent._physicsBodyId = runtime._physics.addBody('box', [0.5, 0.5, 0.5], ent.position, mt, { rotation: ent.rotation, mass: ent.mass })
103
+ }
99
104
  } else {
100
105
  throw err
101
106
  }
@@ -1,66 +1,184 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
 
3
+ let _dracoDecoderPromise = null
4
+
5
+ async function getDracoDecoder() {
6
+ if (!_dracoDecoderPromise) {
7
+ try {
8
+ const dracoGltf = await import('draco3dgltf')
9
+ _dracoDecoderPromise = dracoGltf.createDecoderModule()
10
+ } catch(e) {
11
+ throw new Error(`Failed to load Draco decoder: ${e.message}`)
12
+ }
13
+ }
14
+ return _dracoDecoderPromise
15
+ }
16
+
3
17
  /**
4
18
  * Extract mesh from GLB file for physics collider creation.
5
- *
6
- * NOTE: Draco-compressed meshes require decompression before vertex extraction.
7
- * This function detects Draco compression and provides clear error guidance.
8
- *
19
+ * Supports both standard and Draco-compressed meshes.
20
+ *
9
21
  * @param {string} filepath - Path to GLB file
10
22
  * @param {number} meshIndex - Mesh index (default 0)
11
23
  * @returns {Object} {vertices, indices, vertexCount, triangleCount, name}
12
- * @throws {Error} If mesh is Draco-compressed or invalid
24
+ * @throws {Error} If mesh cannot be extracted
13
25
  */
14
26
  export function extractMeshFromGLB(filepath, meshIndex = 0) {
27
+ console.log(`[GLBLoader] Extracting from: ${filepath}`)
15
28
  const buf = readFileSync(filepath)
16
29
  if (buf.toString('ascii', 0, 4) !== 'glTF') throw new Error('Not a GLB file')
17
-
30
+
18
31
  const jsonLen = buf.readUInt32LE(12)
19
32
  const json = JSON.parse(buf.toString('utf-8', 20, 20 + jsonLen))
20
33
  const binOffset = 20 + jsonLen + 8
21
-
34
+
22
35
  const mesh = json.meshes[meshIndex]
23
36
  if (!mesh) throw new Error(`Mesh index ${meshIndex} not found`)
24
-
37
+
25
38
  const prim = mesh.primitives[0]
26
-
27
- // Check for Draco compression - this is the critical issue
39
+
40
+ // Check for Draco compression and defer to async handler
28
41
  if (prim.extensions?.KHR_draco_mesh_compression) {
29
- const dracoExt = prim.extensions.KHR_draco_mesh_compression
30
- const bufViewIdx = dracoExt.bufferView
31
- throw new Error(
32
- `Cannot extract collider from Draco-compressed mesh '${mesh.name}'. \n\n` +
33
- `SOLUTION: Use gltfpack to decompress the model before physics import:\n` +
34
- ` gltfpack -i model.glb -o model-uncompressed.glb -noq\n\n` +
35
- `Or mark this model as 'no-physics' in entity config and use trigger colliders instead.`
36
- )
42
+ throw new Error('Draco-compressed mesh detected. Use extractMeshFromGLBAsync() instead.')
37
43
  }
38
-
44
+
39
45
  // Standard uncompressed GLB mesh extraction
40
46
  const posAcc = json.accessors[prim.attributes.POSITION]
41
47
  const posView = json.bufferViews[posAcc.bufferView]
42
48
  const posOff = binOffset + (posView.byteOffset || 0) + (posAcc.byteOffset || 0)
43
- const vertices = new Float32Array(buf.buffer.slice(buf.byteOffset + posOff, buf.byteOffset + posOff + posAcc.count * 12))
44
-
49
+ const vertices = new Float32Array(buf.buffer.slice(posOff, posOff + posAcc.count * 12))
50
+
51
+ let indices = null
52
+ if (prim.indices !== undefined) {
53
+ const idxAcc = json.accessors[prim.indices]
54
+ const idxView = json.bufferViews[idxAcc.bufferView]
55
+ const idxOff = binOffset + (idxView.byteOffset || 0) + (idxAcc.byteOffset || 0)
56
+ if (idxAcc.componentType === 5123) {
57
+ const raw = new Uint16Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 2))
58
+ indices = new Uint32Array(raw)
59
+ } else {
60
+ indices = new Uint32Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 4))
61
+ }
62
+ }
63
+
64
+ return {
65
+ vertices,
66
+ indices,
67
+ vertexCount: posAcc.count,
68
+ triangleCount: indices ? indices.length / 3 : 0,
69
+ name: mesh.name
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Extract mesh from GLB file with Draco decompression support.
75
+ * Handles both compressed and uncompressed meshes asynchronously.
76
+ *
77
+ * @param {string} filepath - Path to GLB file
78
+ * @param {number} meshIndex - Mesh index (default 0)
79
+ * @returns {Promise<Object>} {vertices, indices, vertexCount, triangleCount, name}
80
+ */
81
+ export async function extractMeshFromGLBAsync(filepath, meshIndex = 0) {
82
+ console.log(`[GLBLoader] Extracting (async) from: ${filepath}`)
83
+ const buf = readFileSync(filepath)
84
+ if (buf.toString('ascii', 0, 4) !== 'glTF') throw new Error('Not a GLB file')
85
+
86
+ const jsonLen = buf.readUInt32LE(12)
87
+ const json = JSON.parse(buf.toString('utf-8', 20, 20 + jsonLen))
88
+ const binOffset = 20 + jsonLen + 8
89
+
90
+ const mesh = json.meshes[meshIndex]
91
+ if (!mesh) throw new Error(`Mesh index ${meshIndex} not found`)
92
+
93
+ const prim = mesh.primitives[0]
94
+
95
+ // Handle Draco-compressed mesh
96
+ if (prim.extensions?.KHR_draco_mesh_compression) {
97
+ return decompressDracoMesh(buf, json, prim, binOffset, mesh.name)
98
+ }
99
+
100
+ // Standard uncompressed extraction
101
+ const posAcc = json.accessors[prim.attributes.POSITION]
102
+ const posView = json.bufferViews[posAcc.bufferView]
103
+ const posOff = binOffset + (posView.byteOffset || 0) + (posAcc.byteOffset || 0)
104
+ const vertices = new Float32Array(buf.buffer.slice(posOff, posOff + posAcc.count * 12))
105
+
45
106
  let indices = null
46
107
  if (prim.indices !== undefined) {
47
108
  const idxAcc = json.accessors[prim.indices]
48
109
  const idxView = json.bufferViews[idxAcc.bufferView]
49
110
  const idxOff = binOffset + (idxView.byteOffset || 0) + (idxAcc.byteOffset || 0)
50
111
  if (idxAcc.componentType === 5123) {
51
- const raw = new Uint16Array(buf.buffer.slice(buf.byteOffset + idxOff, buf.byteOffset + idxOff + idxAcc.count * 2))
112
+ const raw = new Uint16Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 2))
52
113
  indices = new Uint32Array(raw)
53
114
  } else {
54
- indices = new Uint32Array(buf.buffer.slice(buf.byteOffset + idxOff, buf.byteOffset + idxOff + idxAcc.count * 4))
115
+ indices = new Uint32Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 4))
55
116
  }
56
117
  }
57
-
58
- return {
59
- vertices,
60
- indices,
61
- vertexCount: posAcc.count,
62
- triangleCount: indices ? indices.length / 3 : 0,
63
- name: mesh.name
118
+
119
+ return {
120
+ vertices,
121
+ indices,
122
+ vertexCount: posAcc.count,
123
+ triangleCount: indices ? indices.length / 3 : 0,
124
+ name: mesh.name
125
+ }
126
+ }
127
+
128
+ async function decompressDracoMesh(buf, json, prim, binOffset, meshName) {
129
+ const decoder = await getDracoDecoder()
130
+
131
+ const dracoExt = prim.extensions.KHR_draco_mesh_compression
132
+ const bufViewIdx = dracoExt.bufferView
133
+ const bufView = json.bufferViews[bufViewIdx]
134
+ const offset = binOffset + (bufView.byteOffset || 0)
135
+ const length = bufView.byteLength
136
+ const dracoData = buf.slice(offset, offset + length)
137
+
138
+ // Create decoder and buffer
139
+ const d = new decoder.Decoder()
140
+ const db = new decoder.DecoderBuffer()
141
+
142
+ try {
143
+ // Initialize buffer
144
+ const dracoArray = new Uint8Array(dracoData)
145
+ db.Init(dracoArray, dracoArray.length)
146
+
147
+ // Decode mesh
148
+ const decodedGeom = d.DecodeBufferToMesh(db)
149
+ if (!decodedGeom) {
150
+ throw new Error('Draco decompression failed: empty result')
151
+ }
152
+
153
+ // Get position attribute
154
+ const posAttrId = d.GetAttributeIdByName(decodedGeom, 'POSITION')
155
+ if (posAttrId < 0) {
156
+ throw new Error('No POSITION attribute in decompressed mesh')
157
+ }
158
+
159
+ const posAttr = d.GetAttribute(decodedGeom, posAttrId)
160
+ const posData = d.GetAttributeFloatForAllPoints(decodedGeom, posAttr)
161
+ const vertices = new Float32Array(posData)
162
+
163
+ // Get indices if available
164
+ let indices = null
165
+ if (decodedGeom.num_faces() > 0) {
166
+ const indicesData = d.GetTrianglesUInt32Array(decodedGeom, decodedGeom.num_faces())
167
+ indices = new Uint32Array(indicesData)
168
+ }
169
+
170
+ decoder.destroy(decodedGeom)
171
+
172
+ return {
173
+ vertices,
174
+ indices,
175
+ vertexCount: decodedGeom.num_points(),
176
+ triangleCount: decodedGeom.num_faces(),
177
+ name: meshName
178
+ }
179
+ } finally {
180
+ decoder.destroy(d)
181
+ decoder.destroy(db)
64
182
  }
65
183
  }
66
184