spoint 0.1.42 → 0.1.44

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 CHANGED
@@ -239,6 +239,14 @@ ctx.physics.addCapsuleCollider(radius, fullHeight)
239
239
  ctx.physics.addTrimeshCollider()
240
240
  // Builds static mesh from entity.model path. Static only.
241
241
 
242
+ ctx.physics.addConvexCollider(points)
243
+ // points: flat Float32Array or Array of [x,y,z,x,y,z,...] vertex positions
244
+ // Builds a ConvexHullShape from the provided point cloud. Supports all motion types.
245
+
246
+ ctx.physics.addConvexFromModel(meshIndex = 0)
247
+ // Extracts vertex positions from entity.model GLB and builds ConvexHullShape.
248
+ // Simpler and faster than trimesh for dynamic objects like vehicles/crates.
249
+
242
250
  ctx.physics.addForce([fx, fy, fz]) // Impulse: velocity += force / mass
243
251
  ctx.physics.setVelocity([vx, vy, vz]) // Set velocity directly
244
252
  ```
@@ -544,6 +552,12 @@ ctx.physics.addCapsuleCollider(radius, fullHeight)
544
552
  ctx.physics.addTrimeshCollider()
545
553
  // Static trimesh from entity.model. Only for static bodies.
546
554
 
555
+ ctx.physics.addConvexCollider(points)
556
+ // points: flat array [x,y,z,x,y,z,...]. Supports all motion types (dynamic/kinematic/static).
557
+
558
+ ctx.physics.addConvexFromModel(meshIndex = 0)
559
+ // Extracts vertices from entity.model GLB and builds ConvexHullShape. Good for dynamic vehicles/crates.
560
+
547
561
  ctx.physics.addForce([fx, fy, fz]) // velocity += force / mass
548
562
  ctx.physics.setVelocity([vx, vy, vz])
549
563
  ```
@@ -1068,6 +1082,14 @@ The engine manually applies `gravity[1] * dt` to Y velocity. This is already han
1068
1082
 
1069
1083
  `addTrimeshCollider()` creates a static mesh. No dynamic or kinematic trimesh support.
1070
1084
 
1085
+ ### Convex hull for dynamic objects
1086
+
1087
+ Use `addConvexCollider(points)` or `addConvexFromModel()` for dynamic/kinematic bodies that need shape-accurate physics (vehicles, crates). Convex hulls support all motion types unlike trimesh. `addConvexFromModel()` reads vertices from the entity's GLB at setup time - call it after setting `entity.model`.
1088
+
1089
+ ### Animation library is cached globally
1090
+
1091
+ `loadAnimationLibrary()` loads `/anim-lib.glb` only once and caches the result. All subsequent calls return the cached result immediately. The library is also pre-fetched in parallel with the VRM download during initialization.
1092
+
1071
1093
  ### Tick drops under load
1072
1094
 
1073
1095
  TickSystem processes max 4 ticks per loop. If the server falls more than 4 ticks behind (31ms at 128 TPS), those ticks are dropped silently.
@@ -123,12 +123,23 @@ function normalizeClips(gltf, vrmVersion, vrmHumanoid) {
123
123
  return clips
124
124
  }
125
125
 
126
+ let _gltfPromise = null
127
+ let _normalizedCache = null
128
+
129
+ export function preloadAnimationLibrary() {
130
+ if (_gltfPromise) return _gltfPromise
131
+ _gltfPromise = new GLTFLoader().loadAsync('/anim-lib.glb')
132
+ return _gltfPromise
133
+ }
134
+
126
135
  export async function loadAnimationLibrary(vrmVersion, vrmHumanoid) {
127
- const loader = new GLTFLoader()
128
- const gltf = await loader.loadAsync('/anim-lib.glb')
136
+ if (_normalizedCache) return _normalizedCache
137
+ const gltf = await preloadAnimationLibrary()
138
+ if (_normalizedCache) return _normalizedCache
129
139
  const normalizedClips = normalizeClips(gltf, vrmVersion || '1', vrmHumanoid)
130
140
  console.log(`[anim] Loaded animation library (${normalizedClips.size} clips):`, [...normalizedClips.keys()])
131
- return { normalizedClips }
141
+ _normalizedCache = { normalizedClips }
142
+ return _normalizedCache
132
143
  }
133
144
 
134
145
  export function createPlayerAnimator(vrm, allClips, vrmVersion, animConfig = {}) {
package/client/app.js CHANGED
@@ -9,7 +9,7 @@ import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm'
9
9
  import { PhysicsNetworkClient, InputHandler, MSG } from '/src/index.client.js'
10
10
  import { createElement, applyDiff } from 'webjsx'
11
11
  import { createCameraController } from './camera.js'
12
- import { loadAnimationLibrary, createPlayerAnimator } from './animation.js'
12
+ import { loadAnimationLibrary, preloadAnimationLibrary, createPlayerAnimator } from './animation.js'
13
13
  import { initFacialSystem } from './facial-animation.js'
14
14
  import { VRButton } from 'three/addons/webxr/VRButton.js'
15
15
  import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'
@@ -713,12 +713,11 @@ function detectVrmVersion(buffer) {
713
713
 
714
714
  function initAssets(playerModelUrl) {
715
715
  loadingMgr.setStage('DOWNLOAD')
716
- assetsReady = loadingMgr.fetchWithProgress(playerModelUrl).then(b => {
716
+ preloadAnimationLibrary()
717
+ assetsReady = loadingMgr.fetchWithProgress(playerModelUrl).then(async b => {
717
718
  vrmBuffer = b
718
719
  loadingMgr.setStage('PROCESS')
719
- return loadAnimationLibrary(detectVrmVersion(b), null)
720
- }).then(result => {
721
- animAssets = result
720
+ animAssets = await loadAnimationLibrary(detectVrmVersion(b), null)
722
721
  assetsLoaded = true
723
722
  checkAllLoaded()
724
723
  }).catch(err => {
@@ -840,7 +839,16 @@ function evaluateAppModule(code) {
840
839
  try {
841
840
  let stripped = code.replace(/^import\s+.*$/gm, '')
842
841
  stripped = stripped.replace(/const\s+__dirname\s*=.*import\.meta\.url.*$/gm, 'const __dirname = "/"')
843
- const wrapped = stripped.replace(/export\s+default\s*/, 'return ').replace(/export\s+/g, '')
842
+ stripped = stripped.replace(/export\s+/g, '')
843
+ const exportDefaultIdx = stripped.search(/\bdefault\s*[\{(]/)
844
+ let wrapped
845
+ if (exportDefaultIdx !== -1) {
846
+ const before = stripped.slice(0, exportDefaultIdx)
847
+ const after = stripped.slice(exportDefaultIdx + 'default'.length).trimStart()
848
+ wrapped = before + '\nreturn ' + after + '\n//# sourceURL=app-module.js'
849
+ } else {
850
+ wrapped = stripped.replace(/\bdefault\s*/, 'return ') + '\n//# sourceURL=app-module.js'
851
+ }
844
852
  const join = (...parts) => parts.filter(Boolean).join('/')
845
853
  const readdirSync = () => []
846
854
  const statSync = () => ({ isDirectory: () => false })
@@ -969,6 +977,7 @@ function loadEntityModel(entityId, entityState) {
969
977
  fitShadowFrustum()
970
978
  pendingLoads.delete(entityId)
971
979
  if (!environmentLoaded) { environmentLoaded = true; checkAllLoaded() }
980
+ if (loadingScreenHidden) renderer.compileAsync(scene, camera).catch(() => renderer.compile(scene, camera))
972
981
  }, (progress) => { if (progress.total > 0) console.log('[gltf]', url, Math.round(progress.loaded / progress.total * 100) + '%') }, (err) => { console.error('[gltf]', url, err); pendingLoads.delete(entityId) })
973
982
  }
974
983
 
@@ -1117,7 +1126,8 @@ function checkAllLoaded() {
1117
1126
  loadingMgr.setStage('INIT')
1118
1127
  loadingMgr.complete()
1119
1128
  loadingScreenHidden = true
1120
- warmupShaders().then(() => loadingScreen.hide()).catch(() => loadingScreen.hide())
1129
+ loadingScreen.hide()
1130
+ warmupShaders().catch(() => {})
1121
1131
  }
1122
1132
 
1123
1133
  function initInputHandler() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,4 +1,5 @@
1
1
  import { CliDebugger } from '../debug/CliDebugger.js'
2
+ import { extractMeshFromGLB } from '../physics/GLBLoader.js'
2
3
 
3
4
  export class AppContext {
4
5
  constructor(entity, runtime) {
@@ -75,6 +76,23 @@ export class AppContext {
75
76
  ent._physicsBodyId = bodyId
76
77
  }
77
78
  },
79
+ addConvexCollider: (points) => {
80
+ ent.collider = { type: 'convex', points }
81
+ if (runtime._physics) {
82
+ const mt = ent.bodyType === 'dynamic' ? 'dynamic' : ent.bodyType === 'kinematic' ? 'kinematic' : 'static'
83
+ ent._physicsBodyId = runtime._physics.addBody('convex', points, ent.position, mt, { rotation: ent.rotation, mass: ent.mass })
84
+ }
85
+ },
86
+ addConvexFromModel: (meshIndex = 0) => {
87
+ if (!ent.model) return
88
+ const mesh = extractMeshFromGLB(runtime.resolveAssetPath(ent.model), meshIndex)
89
+ const points = Array.from(mesh.vertices)
90
+ ent.collider = { type: 'convex', points }
91
+ if (runtime._physics) {
92
+ const mt = ent.bodyType === 'dynamic' ? 'dynamic' : ent.bodyType === 'kinematic' ? 'kinematic' : 'static'
93
+ ent._physicsBodyId = runtime._physics.addBody('convex', points, ent.position, mt, { rotation: ent.rotation, mass: ent.mass })
94
+ }
95
+ },
78
96
  addForce: (f) => {
79
97
  const mass = ent.mass || 1
80
98
  ent.velocity[0] += f[0] / mass
@@ -59,6 +59,13 @@ export class PhysicsWorld {
59
59
  if (shapeType === 'box') shape = new J.BoxShape(new J.Vec3(params[0], params[1], params[2]), 0.05, null)
60
60
  else if (shapeType === 'sphere') shape = new J.SphereShape(params)
61
61
  else if (shapeType === 'capsule') shape = new J.CapsuleShape(params[1], params[0])
62
+ else if (shapeType === 'convex') {
63
+ const pts = new J.VertexList()
64
+ for (let i = 0; i < params.length; i += 3) pts.push_back(new J.Float3(params[i], params[i + 1], params[i + 2]))
65
+ const cvx = new J.ConvexHullShapeSettings(pts)
66
+ shape = cvx.Create().Get()
67
+ J.destroy(pts); J.destroy(cvx)
68
+ }
62
69
  else return null
63
70
  const mt = motionType === 'dynamic' ? J.EMotionType_Dynamic : motionType === 'kinematic' ? J.EMotionType_Kinematic : J.EMotionType_Static
64
71
  layer = motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC