spoint 0.1.77 → 0.1.79
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,14 +25,9 @@ export default {
|
|
|
25
25
|
server: {
|
|
26
26
|
setup(ctx) {
|
|
27
27
|
ctx.physics.setStatic(true)
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} catch (e) {
|
|
32
|
-
// Fallback if convex hull extraction fails
|
|
33
|
-
console.log(`[Environment] Convex extraction failed: ${e.message}`); console.log(e.stack)
|
|
34
|
-
ctx.physics.addBoxCollider([50, 0.5, 50])
|
|
35
|
-
}
|
|
28
|
+
// Use simple box collider for environment - trimesh creation is too slow for large meshes
|
|
29
|
+
// The environment is primarily visual; players collide with box bounds instead
|
|
30
|
+
ctx.physics.addBoxCollider([50, 15, 50])
|
|
36
31
|
|
|
37
32
|
ctx.state.smartObjects = new Map()
|
|
38
33
|
ctx.state.editorMode = false
|
|
Binary file
|
package/apps/world/index.js
CHANGED
|
@@ -58,7 +58,7 @@ export default {
|
|
|
58
58
|
fadeTime: 0.15
|
|
59
59
|
},
|
|
60
60
|
entities: [
|
|
61
|
-
{ id: 'environment', model: './apps/tps-game/schwust
|
|
61
|
+
{ id: 'environment', model: './apps/tps-game/schwust.glb', position: [0, -10, 0], app: 'environment' },
|
|
62
62
|
{ id: 'game', position: [0, 0, 0], app: 'tps-game' },
|
|
63
63
|
{ id: 'power-crates', position: [0, 0, 0], app: 'power-crate' },
|
|
64
64
|
{ id: 'interact-box', position: [-100, 3, -100], app: 'interactable' }
|
package/package.json
CHANGED
package/src/apps/AppContext.js
CHANGED
|
@@ -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]
|
|
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
|
}
|
package/src/physics/GLBLoader.js
CHANGED
|
@@ -1,48 +1,53 @@
|
|
|
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
|
-
*
|
|
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
|
|
24
|
+
* @throws {Error} If mesh cannot be extracted
|
|
13
25
|
*/
|
|
14
26
|
export function extractMeshFromGLB(filepath, meshIndex = 0) {
|
|
15
27
|
console.log(`[GLBLoader] Extracting from: ${filepath}`)
|
|
16
28
|
const buf = readFileSync(filepath)
|
|
17
29
|
if (buf.toString('ascii', 0, 4) !== 'glTF') throw new Error('Not a GLB file')
|
|
18
|
-
|
|
30
|
+
|
|
19
31
|
const jsonLen = buf.readUInt32LE(12)
|
|
20
32
|
const json = JSON.parse(buf.toString('utf-8', 20, 20 + jsonLen))
|
|
21
33
|
const binOffset = 20 + jsonLen + 8
|
|
22
|
-
|
|
34
|
+
|
|
23
35
|
const mesh = json.meshes[meshIndex]
|
|
24
36
|
if (!mesh) throw new Error(`Mesh index ${meshIndex} not found`)
|
|
25
|
-
|
|
37
|
+
|
|
26
38
|
const prim = mesh.primitives[0]
|
|
27
|
-
|
|
28
|
-
// Check for Draco compression
|
|
39
|
+
|
|
40
|
+
// Check for Draco compression and defer to async handler
|
|
29
41
|
if (prim.extensions?.KHR_draco_mesh_compression) {
|
|
30
|
-
|
|
31
|
-
const bufViewIdx = dracoExt.bufferView
|
|
32
|
-
throw new Error(
|
|
33
|
-
`Cannot extract collider from Draco-compressed mesh '${mesh.name}'. \n\n` +
|
|
34
|
-
`SOLUTION: Use gltfpack to decompress the model before physics import:\n` +
|
|
35
|
-
` gltfpack -i model.glb -o model-uncompressed.glb -noq\n\n` +
|
|
36
|
-
`Or mark this model as 'no-physics' in entity config and use trigger colliders instead.`
|
|
37
|
-
)
|
|
42
|
+
throw new Error('Draco-compressed mesh detected. Use extractMeshFromGLBAsync() instead.')
|
|
38
43
|
}
|
|
39
|
-
|
|
44
|
+
|
|
40
45
|
// Standard uncompressed GLB mesh extraction
|
|
41
46
|
const posAcc = json.accessors[prim.attributes.POSITION]
|
|
42
47
|
const posView = json.bufferViews[posAcc.bufferView]
|
|
43
48
|
const posOff = binOffset + (posView.byteOffset || 0) + (posAcc.byteOffset || 0)
|
|
44
49
|
const vertices = new Float32Array(buf.buffer.slice(posOff, posOff + posAcc.count * 12))
|
|
45
|
-
|
|
50
|
+
|
|
46
51
|
let indices = null
|
|
47
52
|
if (prim.indices !== undefined) {
|
|
48
53
|
const idxAcc = json.accessors[prim.indices]
|
|
@@ -55,13 +60,125 @@ export function extractMeshFromGLB(filepath, meshIndex = 0) {
|
|
|
55
60
|
indices = new Uint32Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 4))
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
vertices,
|
|
61
|
-
indices,
|
|
62
|
-
vertexCount: posAcc.count,
|
|
63
|
-
triangleCount: indices ? indices.length / 3 : 0,
|
|
64
|
-
name: mesh.name
|
|
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
|
+
|
|
106
|
+
let indices = null
|
|
107
|
+
if (prim.indices !== undefined) {
|
|
108
|
+
const idxAcc = json.accessors[prim.indices]
|
|
109
|
+
const idxView = json.bufferViews[idxAcc.bufferView]
|
|
110
|
+
const idxOff = binOffset + (idxView.byteOffset || 0) + (idxAcc.byteOffset || 0)
|
|
111
|
+
if (idxAcc.componentType === 5123) {
|
|
112
|
+
const raw = new Uint16Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 2))
|
|
113
|
+
indices = new Uint32Array(raw)
|
|
114
|
+
} else {
|
|
115
|
+
indices = new Uint32Array(buf.buffer.slice(idxOff, idxOff + idxAcc.count * 4))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
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)
|
|
65
182
|
}
|
|
66
183
|
}
|
|
67
184
|
|
|
Binary file
|