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.
- package/client/animation.js +7 -6
- package/client/app.js +36 -28
- package/package.json +1 -1
- package/src/sdk/StaticHandler.js +16 -5
package/client/animation.js
CHANGED
|
@@ -46,15 +46,15 @@ function filterUpperBodyTracks(clip) {
|
|
|
46
46
|
return new THREE.AnimationClip(clip.name, clip.duration, filteredTracks)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function
|
|
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,
|
|
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
|
-
|
|
779
|
-
features.lookAt.lookAt(
|
|
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
|
|
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
|
|
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(
|
|
924
|
-
|
|
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(
|
|
936
|
-
|
|
937
|
-
|
|
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.
|
|
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(
|
|
988
|
-
if (mesh && e.rotation) mesh.quaternion.set(
|
|
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 (
|
|
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
|
-
|
|
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
|
|
1393
|
-
if (
|
|
1394
|
-
const
|
|
1395
|
-
if (
|
|
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 =
|
|
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
package/src/sdk/StaticHandler.js
CHANGED
|
@@ -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
|
-
|
|
35
|
-
if (
|
|
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)
|