spoint 0.1.27 → 0.1.29

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.
@@ -46,15 +46,15 @@ function filterUpperBodyTracks(clip) {
46
46
  return new THREE.AnimationClip(clip.name, clip.duration, filteredTracks)
47
47
  }
48
48
 
49
- function filterValidClipTracks(clip, targetObj) {
50
- // Get all bone/mesh names that exist in target
49
+ function buildValidBoneSet(targetObj) {
51
50
  const validBones = new Set()
52
51
  targetObj.traverse(child => {
53
- if (child.isBone || child.isSkinnedMesh) {
54
- validBones.add(child.name)
55
- }
52
+ if (child.isBone || child.isSkinnedMesh) validBones.add(child.name)
56
53
  })
54
+ return validBones
55
+ }
57
56
 
57
+ function filterValidClipTracks(clip, validBones) {
58
58
  const validTracks = clip.tracks.filter(track => {
59
59
  const boneName = extractBoneName(track.name)
60
60
  if (!validBones.has(boneName)) {
@@ -140,6 +140,7 @@ export function createPlayerAnimator(vrm, allClips, vrmVersion, animConfig = {})
140
140
  const additiveActions = new Map()
141
141
 
142
142
  const clips = allClips.normalizedClips || allClips.rawClips || allClips
143
+ const validBones = buildValidBoneSet(root)
143
144
 
144
145
  for (const [name, clip] of clips) {
145
146
  if (!STATES[name]) continue
@@ -149,7 +150,7 @@ export function createPlayerAnimator(vrm, allClips, vrmVersion, animConfig = {})
149
150
  console.log(`[anim] ${name} tracks:`, clip.tracks.map(t => extractBoneName(t.name)))
150
151
  }
151
152
 
152
- let playClip = filterValidClipTracks(clip, root)
153
+ let playClip = filterValidClipTracks(clip, validBones)
153
154
 
154
155
  if (cfg.upperBody) {
155
156
  const upperBodyClip = filterUpperBodyTracks(playClip)
package/client/app.js CHANGED
@@ -659,9 +659,11 @@ scene.add(ground)
659
659
 
660
660
  const loadingManager = new THREE.LoadingManager()
661
661
  loadingManager.onError = (url) => console.warn('[THREE] Failed to load:', url)
662
+ THREE.Cache.enabled = true
662
663
  const gltfLoader = new GLTFLoader(loadingManager)
663
664
  const dracoLoader = new DRACOLoader(loadingManager)
664
665
  dracoLoader.setDecoderPath('/draco/')
666
+ dracoLoader.setWorkerLimit(1)
665
667
  gltfLoader.setDRACOLoader(dracoLoader)
666
668
  gltfLoader.register((parser) => new VRMLoaderPlugin(parser))
667
669
  const playerMeshes = new Map()
@@ -770,13 +772,15 @@ function initVRMFeatures(id, vrm) {
770
772
  playerExpressions.set(id, features)
771
773
  }
772
774
 
775
+ const _lookTargetVec = new THREE.Vector3()
776
+
773
777
  function updateVRMFeatures(id, dt, targetPosition) {
774
778
  const features = playerExpressions.get(id)
775
779
  if (!features) return
776
780
  if (features.springBone) features.springBone.update(dt)
777
781
  if (features.lookAt && targetPosition) {
778
- const lookTarget = new THREE.Vector3(targetPosition.x, targetPosition.y + 1.6, targetPosition.z)
779
- features.lookAt.lookAt(lookTarget)
782
+ _lookTargetVec.set(targetPosition.x, targetPosition.y + 1.6, targetPosition.z)
783
+ features.lookAt.lookAt(_lookTargetVec)
780
784
  }
781
785
  if (features.expressions) {
782
786
  features.blinkTimer += dt
@@ -890,18 +894,13 @@ function rebuildEntityHierarchy(entities) {
890
894
  if (!mesh) continue
891
895
 
892
896
  const parentId = entityParentMap.get(e.id)
893
- const currentParent = mesh.parent && mesh.parent !== scene ? mesh.parent : null
894
- const currentParentId = Array.from(entityParentMap.entries()).find(([k, v]) => v === null || entityMeshes.get(k) === currentParent)?.[0]
897
+ const currentParent = mesh.parent !== scene ? mesh.parent : null
895
898
 
896
899
  if (parentId === null) {
897
- if (currentParent && currentParent !== scene) {
898
- scene.add(mesh)
899
- }
900
+ if (currentParent) scene.add(mesh)
900
901
  } else {
901
902
  const parentMesh = entityMeshes.get(parentId)
902
- if (parentMesh && parentMesh !== currentParent) {
903
- parentMesh.add(mesh)
904
- }
903
+ if (parentMesh && parentMesh !== currentParent) parentMesh.add(mesh)
905
904
  }
906
905
  }
907
906
  }
@@ -920,8 +919,8 @@ function loadEntityModel(entityId, entityState) {
920
919
  } else {
921
920
  group = buildEntityMesh(entityId, entityState.custom)
922
921
  }
923
- group.position.set(...entityState.position)
924
- if (entityState.rotation) group.quaternion.set(...entityState.rotation)
922
+ const ep = entityState.position; group.position.set(ep[0], ep[1], ep[2])
923
+ const er = entityState.rotation; if (er) group.quaternion.set(er[0], er[1], er[2], er[3])
925
924
  scene.add(group)
926
925
  entityMeshes.set(entityId, group)
927
926
  pendingLoads.delete(entityId)
@@ -932,13 +931,20 @@ function loadEntityModel(entityId, entityState) {
932
931
  const url = entityState.model.startsWith('./') ? '/' + entityState.model.slice(2) : entityState.model
933
932
  gltfLoader.load(url, (gltf) => {
934
933
  const model = gltf.scene
935
- model.position.set(...entityState.position)
936
- if (entityState.rotation) model.quaternion.set(...entityState.rotation)
937
- model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; if (c.material) { c.material.shadowSide = THREE.DoubleSide; c.material.roughness = 1; c.material.metalness = 0; if (c.material.specularIntensity !== undefined) c.material.specularIntensity = 0 } } })
934
+ const mp = entityState.position; model.position.set(mp[0], mp[1], mp[2])
935
+ const mr = entityState.rotation; if (mr) model.quaternion.set(mr[0], mr[1], mr[2], mr[3])
936
+ const colliders = []
937
+ model.traverse(c => {
938
+ if (c.isMesh) {
939
+ c.castShadow = true
940
+ c.receiveShadow = true
941
+ if (!c.isSkinnedMesh) { c.matrixAutoUpdate = false; colliders.push(c) }
942
+ if (c.material) { c.material.shadowSide = THREE.DoubleSide; c.material.roughness = 1; c.material.metalness = 0; if (c.material.specularIntensity !== undefined) c.material.specularIntensity = 0 }
943
+ }
944
+ })
945
+ model.updateMatrixWorld(true)
938
946
  scene.add(model)
939
947
  entityMeshes.set(entityId, model)
940
- const colliders = []
941
- model.traverse(c => { if (c.isMesh && !c.isSkinnedMesh) colliders.push(c) })
942
948
  cam.setEnvironment(colliders)
943
949
  scene.remove(ground)
944
950
  fitShadowFrustum()
@@ -977,15 +983,17 @@ const client = new PhysicsNetworkClient({
977
983
  const mesh = playerMeshes.get(p.id)
978
984
  const feetOff = mesh?.userData?.feetOffset ?? 1.3
979
985
  const tx = p.position[0], ty = p.position[1] - feetOff, tz = p.position[2]
980
- playerTargets.set(p.id, { x: tx, y: ty, z: tz })
986
+ const existingTarget = playerTargets.get(p.id)
987
+ if (existingTarget) { existingTarget.x = tx; existingTarget.y = ty; existingTarget.z = tz }
988
+ else playerTargets.set(p.id, { x: tx, y: ty, z: tz })
981
989
  playerStates.set(p.id, p)
982
990
  const dx = tx - mesh.position.x, dy = ty - mesh.position.y, dz = tz - mesh.position.z
983
991
  if (!mesh.userData.initialized || dx * dx + dy * dy + dz * dz > 100) { mesh.position.set(tx, ty, tz); mesh.userData.initialized = true }
984
992
  }
985
993
  for (const e of smoothState.entities) {
986
994
  const mesh = entityMeshes.get(e.id)
987
- if (mesh && e.position) mesh.position.set(...e.position)
988
- if (mesh && e.rotation) mesh.quaternion.set(...e.rotation)
995
+ if (mesh && e.position) mesh.position.set(e.position[0], e.position[1], e.position[2])
996
+ if (mesh && e.rotation) mesh.quaternion.set(e.rotation[0], e.rotation[1], e.rotation[2], e.rotation[3])
989
997
  if (!entityMeshes.has(e.id)) loadEntityModel(e.id, e)
990
998
  }
991
999
  rebuildEntityHierarchy(smoothState.entities)
@@ -1379,21 +1387,21 @@ function animate(timestamp) {
1379
1387
  const mesh = playerMeshes.get(id)
1380
1388
  if (!mesh) continue
1381
1389
  const vx = ps.velocity?.[0] || 0, vz = ps.velocity?.[2] || 0
1382
- if (Math.sqrt(vx * vx + vz * vz) > 0.5) mesh.userData.lastYaw = Math.atan2(vx, vz)
1390
+ if (vx * vx + vz * vz > 0.25) mesh.userData.lastYaw = Math.atan2(vx, vz)
1383
1391
  if (mesh.userData.lastYaw !== undefined) {
1384
1392
  let diff = mesh.userData.lastYaw - mesh.rotation.y
1385
- while (diff > Math.PI) diff -= Math.PI * 2
1386
- while (diff < -Math.PI) diff += Math.PI * 2
1393
+ diff = diff - Math.PI * 2 * Math.round(diff / (Math.PI * 2))
1387
1394
  mesh.rotation.y += diff * lerpFactor
1388
1395
  }
1389
1396
  const target = playerTargets.get(id)
1390
1397
  updateVRMFeatures(id, frameDt, target)
1391
1398
  if (id !== client.playerId && ps.lookPitch !== undefined) {
1392
- const vrm = playerVrms.get(id)
1393
- if (vrm?.humanoid) {
1394
- const head = vrm.humanoid.getNormalizedBoneNode('head')
1395
- if (head) head.rotation.x = -(ps.lookPitch || 0) * 0.6
1399
+ const features = playerExpressions.get(id)
1400
+ if (features && !features._headBone) {
1401
+ const vrm = playerVrms.get(id)
1402
+ if (vrm?.humanoid) features._headBone = vrm.humanoid.getNormalizedBoneNode('head')
1396
1403
  }
1404
+ if (features?._headBone) features._headBone.rotation.x = -(ps.lookPitch || 0) * 0.6
1397
1405
  }
1398
1406
  }
1399
1407
  for (const [eid, mesh] of entityMeshes) {
@@ -1408,7 +1416,7 @@ function animate(timestamp) {
1408
1416
  if (engineCtx.facial) engineCtx.facial.update(frameDt)
1409
1417
  uiTimer += frameDt
1410
1418
  if (latestState && uiTimer >= 0.25) { uiTimer = 0; renderAppUI(latestState) }
1411
- const local = client.state?.players?.find(p => p.id === client.playerId)
1419
+ const local = playerStates.get(client.playerId)
1412
1420
  const inVR = renderer.xr.isPresenting
1413
1421
  if (!inVR || cam.getEditMode()) {
1414
1422
  cam.update(local, playerMeshes.get(client.playerId), frameDt, latestInput)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -10,6 +10,20 @@ const MIME_TYPES = {
10
10
  }
11
11
 
12
12
  const GZIP_EXTENSIONS = new Set(['.glb', '.vrm', '.gltf', '.js', '.css', '.html', '.json'])
13
+ const fileCache = new Map()
14
+
15
+ function getCached(fp, ext) {
16
+ const mtime = statSync(fp).mtimeMs
17
+ const key = fp
18
+ const cached = fileCache.get(key)
19
+ if (cached && cached.mtime === mtime) return cached
20
+ let raw = readFileSync(fp)
21
+ const shouldGzip = GZIP_EXTENSIONS.has(ext) && raw.length > 100
22
+ const content = shouldGzip ? gzipSync(raw) : raw
23
+ const entry = { mtime, content, gzipped: shouldGzip }
24
+ fileCache.set(key, entry)
25
+ return entry
26
+ }
13
27
 
14
28
  export function createStaticHandler(dirs) {
15
29
  return (req, res) => {
@@ -31,11 +45,8 @@ export function createStaticHandler(dirs) {
31
45
  } else if (ext === '.glb' || ext === '.vrm' || ext === '.gltf') {
32
46
  headers['Cache-Control'] = 'public, max-age=86400, immutable'
33
47
  }
34
- let content = readFileSync(fp)
35
- if (GZIP_EXTENSIONS.has(ext) && content.length > 100) {
36
- content = gzipSync(content)
37
- headers['Content-Encoding'] = 'gzip'
38
- }
48
+ const { content, gzipped } = getCached(fp, ext)
49
+ if (gzipped) headers['Content-Encoding'] = 'gzip'
39
50
  headers['Content-Length'] = content.length
40
51
  res.writeHead(200, headers)
41
52
  res.end(content)