@worldmesh/runtime 0.2.0 → 0.2.2
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/README.md +33 -3
- package/dist/audio/footsteps.d.ts +37 -0
- package/dist/audio/footsteps.d.ts.map +1 -0
- package/dist/audio/footsteps.js +178 -0
- package/dist/audio/footsteps.js.map +1 -0
- package/dist/avatar/descriptor.d.ts +15 -0
- package/dist/avatar/descriptor.d.ts.map +1 -1
- package/dist/avatar/descriptor.js +19 -0
- package/dist/avatar/descriptor.js.map +1 -1
- package/dist/avatar/loader.d.ts +12 -1
- package/dist/avatar/loader.d.ts.map +1 -1
- package/dist/avatar/loader.js +16 -1
- package/dist/avatar/loader.js.map +1 -1
- package/dist/avatar/share.d.ts +30 -0
- package/dist/avatar/share.d.ts.map +1 -0
- package/dist/avatar/share.js +203 -0
- package/dist/avatar/share.js.map +1 -0
- package/dist/camera/cameraRig.d.ts +4 -0
- package/dist/camera/cameraRig.d.ts.map +1 -1
- package/dist/camera/cameraRig.js +10 -1
- package/dist/camera/cameraRig.js.map +1 -1
- package/dist/camera/viewHandoff.d.ts +20 -0
- package/dist/camera/viewHandoff.d.ts.map +1 -0
- package/dist/camera/viewHandoff.js +64 -0
- package/dist/camera/viewHandoff.js.map +1 -0
- package/dist/controls/input.d.ts +29 -0
- package/dist/controls/input.d.ts.map +1 -1
- package/dist/controls/input.js +124 -1
- package/dist/controls/input.js.map +1 -1
- package/dist/controls/touch.d.ts +10 -0
- package/dist/controls/touch.d.ts.map +1 -1
- package/dist/controls/touch.js +57 -1
- package/dist/controls/touch.js.map +1 -1
- package/dist/core/worldmesh.d.ts +9 -0
- package/dist/core/worldmesh.d.ts.map +1 -1
- package/dist/core/worldmesh.js +498 -26
- package/dist/core/worldmesh.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/net/follow.d.ts +7 -0
- package/dist/net/follow.d.ts.map +1 -0
- package/dist/net/follow.js +10 -0
- package/dist/net/follow.js.map +1 -0
- package/dist/net/presence.d.ts +126 -4
- package/dist/net/presence.d.ts.map +1 -1
- package/dist/net/presence.js +472 -25
- package/dist/net/presence.js.map +1 -1
- package/dist/net/roomFull.d.ts +44 -0
- package/dist/net/roomFull.d.ts.map +1 -0
- package/dist/net/roomFull.js +147 -0
- package/dist/net/roomFull.js.map +1 -0
- package/dist/player/avatar.d.ts +17 -4
- package/dist/player/avatar.d.ts.map +1 -1
- package/dist/player/avatar.js +85 -18
- package/dist/player/avatar.js.map +1 -1
- package/dist/player/player.d.ts +6 -2
- package/dist/player/player.d.ts.map +1 -1
- package/dist/player/player.js +13 -3
- package/dist/player/player.js.map +1 -1
- package/dist/player/stroke.d.ts +11 -0
- package/dist/player/stroke.d.ts.map +1 -0
- package/dist/player/stroke.js +96 -0
- package/dist/player/stroke.js.map +1 -0
- package/dist/portals/portals.d.ts +1 -1
- package/dist/portals/portals.d.ts.map +1 -1
- package/dist/portals/portals.js +6 -2
- package/dist/portals/portals.js.map +1 -1
- package/dist/types.d.ts +53 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/overlay.d.ts +18 -0
- package/dist/ui/overlay.d.ts.map +1 -1
- package/dist/ui/overlay.js +166 -1
- package/dist/ui/overlay.js.map +1 -1
- package/dist/xr/session.d.ts +13 -0
- package/dist/xr/session.d.ts.map +1 -0
- package/dist/xr/session.js +33 -0
- package/dist/xr/session.js.map +1 -0
- package/package.json +1 -1
- package/src/audio/footsteps.ts +185 -0
- package/src/avatar/descriptor.ts +28 -0
- package/src/avatar/loader.ts +19 -2
- package/src/avatar/share.ts +196 -0
- package/src/camera/cameraRig.ts +10 -1
- package/src/camera/viewHandoff.ts +67 -0
- package/src/controls/input.ts +119 -1
- package/src/controls/touch.ts +59 -1
- package/src/core/worldmesh.ts +495 -25
- package/src/index.ts +14 -7
- package/src/net/follow.ts +9 -0
- package/src/net/presence.ts +548 -28
- package/src/net/roomFull.ts +160 -0
- package/src/player/avatar.ts +87 -18
- package/src/player/player.ts +13 -4
- package/src/player/stroke.ts +93 -0
- package/src/portals/portals.ts +5 -2
- package/src/types.ts +52 -9
- package/src/ui/overlay.ts +171 -1
- package/src/xr/session.ts +33 -0
package/dist/ui/overlay.js
CHANGED
|
@@ -98,6 +98,49 @@ const CSS = `
|
|
|
98
98
|
border-color: rgba(255,255,255,0.6);
|
|
99
99
|
transform: scale(1.02);
|
|
100
100
|
}
|
|
101
|
+
.wm-vr-launch {
|
|
102
|
+
position: absolute;
|
|
103
|
+
top: max(16px, env(safe-area-inset-top));
|
|
104
|
+
left: 50%;
|
|
105
|
+
transform: translateX(-50%);
|
|
106
|
+
pointer-events: auto;
|
|
107
|
+
font-size: 13px;
|
|
108
|
+
letter-spacing: .06em;
|
|
109
|
+
text-transform: uppercase;
|
|
110
|
+
padding: 10px 22px;
|
|
111
|
+
border: 1px solid rgba(255,255,255,0.4);
|
|
112
|
+
border-radius: 999px;
|
|
113
|
+
background: rgba(8,11,16,0.72);
|
|
114
|
+
color: #fff;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
font-family: inherit;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
touch-action: manipulation;
|
|
119
|
+
-webkit-tap-highlight-color: transparent;
|
|
120
|
+
}
|
|
121
|
+
.wm-vr-launch:hover, .wm-vr-launch:active {
|
|
122
|
+
background: rgba(8,11,16,0.9);
|
|
123
|
+
border-color: rgba(255,255,255,0.7);
|
|
124
|
+
}
|
|
125
|
+
.wm-overlay:not([data-locked="true"]) .wm-vr-launch,
|
|
126
|
+
.wm-overlay[data-xr="true"] .wm-vr-launch,
|
|
127
|
+
.wm-overlay[data-xr="true"] .wm-vr-menu {
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
130
|
+
.wm-vr-menu {
|
|
131
|
+
font-size: 13px;
|
|
132
|
+
letter-spacing: .06em;
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
padding: 10px 22px;
|
|
135
|
+
border: 1px solid rgba(255,255,255,0.35);
|
|
136
|
+
border-radius: 999px;
|
|
137
|
+
background: rgba(255,255,255,0.08);
|
|
138
|
+
color: #fff;
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
font-family: inherit;
|
|
141
|
+
font-weight: 500;
|
|
142
|
+
touch-action: manipulation;
|
|
143
|
+
}
|
|
101
144
|
.wm-keys {
|
|
102
145
|
display: grid;
|
|
103
146
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -198,6 +241,55 @@ const CSS = `
|
|
|
198
241
|
transition: opacity .2s ease;
|
|
199
242
|
}
|
|
200
243
|
.wm-overlay[data-locked="true"] .wm-hint { opacity: 1; }
|
|
244
|
+
.wm-room-full {
|
|
245
|
+
position: absolute;
|
|
246
|
+
inset: 0;
|
|
247
|
+
display: none;
|
|
248
|
+
flex-direction: column;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
gap: 18px;
|
|
252
|
+
background: rgba(8, 11, 16, 0.22);
|
|
253
|
+
backdrop-filter: blur(8px);
|
|
254
|
+
-webkit-backdrop-filter: blur(8px);
|
|
255
|
+
pointer-events: auto;
|
|
256
|
+
z-index: 6;
|
|
257
|
+
padding: max(20px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
|
|
258
|
+
}
|
|
259
|
+
.wm-overlay[data-room-full="true"] .wm-room-full { display: flex; }
|
|
260
|
+
.wm-overlay[data-room-full="true"] .wm-lock { display: none; }
|
|
261
|
+
.wm-room-full-label {
|
|
262
|
+
margin: 0;
|
|
263
|
+
padding: 14px 36px;
|
|
264
|
+
border-radius: 4px;
|
|
265
|
+
background: rgba(160, 12, 18, 0.92);
|
|
266
|
+
color: #fff;
|
|
267
|
+
font-family: Urbanist, ui-sans-serif, system-ui, sans-serif;
|
|
268
|
+
font-size: clamp(22px, 4.4vw, 36px);
|
|
269
|
+
font-weight: 700;
|
|
270
|
+
letter-spacing: .02em;
|
|
271
|
+
text-align: center;
|
|
272
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
|
|
273
|
+
}
|
|
274
|
+
.wm-room-full-retry {
|
|
275
|
+
font-size: 13.5px;
|
|
276
|
+
letter-spacing: .14em;
|
|
277
|
+
text-transform: uppercase;
|
|
278
|
+
padding: 12px 28px;
|
|
279
|
+
border: 1px solid rgba(255,255,255,0.45);
|
|
280
|
+
border-radius: 999px;
|
|
281
|
+
background: rgba(255,255,255,0.12);
|
|
282
|
+
color: #fff;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
font-family: inherit;
|
|
285
|
+
font-weight: 500;
|
|
286
|
+
touch-action: manipulation;
|
|
287
|
+
-webkit-tap-highlight-color: transparent;
|
|
288
|
+
}
|
|
289
|
+
.wm-room-full-retry:hover, .wm-room-full-retry:active {
|
|
290
|
+
background: rgba(255,255,255,0.22);
|
|
291
|
+
border-color: rgba(255,255,255,0.7);
|
|
292
|
+
}
|
|
201
293
|
|
|
202
294
|
@media (max-width: 400px) {
|
|
203
295
|
.wm-lock {
|
|
@@ -273,7 +365,13 @@ export class Overlay {
|
|
|
273
365
|
root;
|
|
274
366
|
prompt;
|
|
275
367
|
lock;
|
|
368
|
+
vrLaunch;
|
|
369
|
+
vrMenu;
|
|
370
|
+
roomFull;
|
|
371
|
+
roomFullLabel;
|
|
372
|
+
roomFullRetry;
|
|
276
373
|
onEnter;
|
|
374
|
+
onRoomFullRetry = null;
|
|
277
375
|
deferLockPanel;
|
|
278
376
|
constructor(options) {
|
|
279
377
|
injectStyles();
|
|
@@ -286,7 +384,7 @@ export class Overlay {
|
|
|
286
384
|
this.root.dataset.locked = options.moveBeforeLock ? 'true' : 'false';
|
|
287
385
|
if (this.deferLockPanel)
|
|
288
386
|
this.root.dataset.deferLock = 'true';
|
|
289
|
-
if (options.crosshair
|
|
387
|
+
if (options.crosshair === true) {
|
|
290
388
|
const crosshair = document.createElement('div');
|
|
291
389
|
crosshair.className = 'wm-crosshair';
|
|
292
390
|
this.root.appendChild(crosshair);
|
|
@@ -325,6 +423,17 @@ export class Overlay {
|
|
|
325
423
|
}
|
|
326
424
|
handleEnter(event);
|
|
327
425
|
});
|
|
426
|
+
const vrMenu = document.createElement('button');
|
|
427
|
+
vrMenu.type = 'button';
|
|
428
|
+
vrMenu.className = 'wm-vr-menu';
|
|
429
|
+
vrMenu.textContent = 'Enter VR';
|
|
430
|
+
vrMenu.hidden = true;
|
|
431
|
+
vrMenu.addEventListener('click', (event) => {
|
|
432
|
+
event.stopPropagation();
|
|
433
|
+
options.onEnterVr?.();
|
|
434
|
+
});
|
|
435
|
+
this.vrMenu = vrMenu;
|
|
436
|
+
content.appendChild(vrMenu);
|
|
328
437
|
if (options.controlsHint !== false) {
|
|
329
438
|
const keys = document.createElement('div');
|
|
330
439
|
keys.className = 'wm-keys';
|
|
@@ -361,6 +470,9 @@ export class Overlay {
|
|
|
361
470
|
const badge = document.createElement('a');
|
|
362
471
|
badge.className = 'wm-badge';
|
|
363
472
|
badge.href = options.hubUrl ?? 'https://worldmesh.net';
|
|
473
|
+
// Shown inside a hub's door: back to the hub replaces the whole page, not the frame.
|
|
474
|
+
if (window.parent !== window)
|
|
475
|
+
badge.target = '_top';
|
|
364
476
|
const dot = document.createElement('span');
|
|
365
477
|
dot.className = 'wm-badge-dot';
|
|
366
478
|
const label = document.createElement('span');
|
|
@@ -372,8 +484,49 @@ export class Overlay {
|
|
|
372
484
|
hint.className = 'wm-hint';
|
|
373
485
|
hint.textContent = isTouch ? 'Tap ⏸ to open the menu' : 'Esc to open the menu';
|
|
374
486
|
this.root.appendChild(hint);
|
|
487
|
+
const vrLaunch = document.createElement('button');
|
|
488
|
+
vrLaunch.type = 'button';
|
|
489
|
+
vrLaunch.className = 'wm-vr-launch';
|
|
490
|
+
vrLaunch.textContent = 'Enter VR';
|
|
491
|
+
vrLaunch.hidden = true;
|
|
492
|
+
vrLaunch.addEventListener('click', (event) => {
|
|
493
|
+
event.stopPropagation();
|
|
494
|
+
options.onEnterVr?.();
|
|
495
|
+
});
|
|
496
|
+
this.vrLaunch = vrLaunch;
|
|
497
|
+
this.root.appendChild(vrLaunch);
|
|
498
|
+
const roomFull = document.createElement('div');
|
|
499
|
+
roomFull.className = 'wm-room-full';
|
|
500
|
+
roomFull.hidden = true;
|
|
501
|
+
const roomFullLabel = document.createElement('p');
|
|
502
|
+
roomFullLabel.className = 'wm-room-full-label';
|
|
503
|
+
const roomFullRetry = document.createElement('button');
|
|
504
|
+
roomFullRetry.type = 'button';
|
|
505
|
+
roomFullRetry.className = 'wm-room-full-retry';
|
|
506
|
+
roomFullRetry.textContent = 'Try again';
|
|
507
|
+
roomFullRetry.hidden = true;
|
|
508
|
+
roomFullRetry.addEventListener('click', (event) => {
|
|
509
|
+
event.stopPropagation();
|
|
510
|
+
this.onRoomFullRetry?.();
|
|
511
|
+
});
|
|
512
|
+
roomFull.append(roomFullLabel, roomFullRetry);
|
|
513
|
+
this.roomFull = roomFull;
|
|
514
|
+
this.roomFullLabel = roomFullLabel;
|
|
515
|
+
this.roomFullRetry = roomFullRetry;
|
|
516
|
+
this.root.appendChild(roomFull);
|
|
375
517
|
document.body.appendChild(this.root);
|
|
376
518
|
}
|
|
519
|
+
/** Show Enter VR once the browser reports immersive-vr support. */
|
|
520
|
+
setVrAvailable(available) {
|
|
521
|
+
this.vrLaunch.hidden = !available;
|
|
522
|
+
this.vrMenu.hidden = !available;
|
|
523
|
+
}
|
|
524
|
+
/** Hide the 2D Enter VR controls while the headset is presenting. */
|
|
525
|
+
setVrPresenting(presenting) {
|
|
526
|
+
this.root.dataset.xr = String(presenting);
|
|
527
|
+
if (presenting)
|
|
528
|
+
this.setLocked(true);
|
|
529
|
+
}
|
|
377
530
|
/** The pause screen, with the control list, is on screen. */
|
|
378
531
|
isPaused() {
|
|
379
532
|
return this.root.dataset.lockReady === 'true' && this.root.dataset.locked !== 'true';
|
|
@@ -402,6 +555,18 @@ export class Overlay {
|
|
|
402
555
|
if (content instanceof HTMLElement)
|
|
403
556
|
content.style.visibility = '';
|
|
404
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Presence refused the join because the room is full. Soft-blur the scene
|
|
560
|
+
* and show the same red banner language as a full world door.
|
|
561
|
+
*/
|
|
562
|
+
setRoomFull(label, options) {
|
|
563
|
+
const full = Boolean(label);
|
|
564
|
+
this.root.dataset.roomFull = String(full);
|
|
565
|
+
this.roomFull.hidden = !full;
|
|
566
|
+
this.roomFullLabel.textContent = label ?? '';
|
|
567
|
+
this.onRoomFullRetry = options?.onRetry ?? null;
|
|
568
|
+
this.roomFullRetry.hidden = !this.onRoomFullRetry;
|
|
569
|
+
}
|
|
405
570
|
setPrompt(text) {
|
|
406
571
|
if (!text) {
|
|
407
572
|
this.prompt.dataset.visible = 'false';
|
package/dist/ui/overlay.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../src/ui/overlay.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAE3C,MAAM,GAAG,GAAG
|
|
1
|
+
{"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../src/ui/overlay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WebGLRenderer } from 'three';
|
|
2
|
+
export declare const IMMERSIVE_VR = "immersive-vr";
|
|
3
|
+
/**
|
|
4
|
+
* Whether this browser can start a WebXR immersive-vr session.
|
|
5
|
+
* Headset browsers resolve true; ordinary desktop Chrome resolves false.
|
|
6
|
+
*/
|
|
7
|
+
export declare function immersiveVrSupported(): Promise<boolean>;
|
|
8
|
+
/**
|
|
9
|
+
* Ask the browser for an immersive VR session and hand it to Three.js.
|
|
10
|
+
* Must run from a user gesture (the Enter VR button).
|
|
11
|
+
*/
|
|
12
|
+
export declare function requestImmersiveVr(renderer: WebGLRenderer): Promise<XRSession>;
|
|
13
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/xr/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;GAGG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAQ7D;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,SAAS,CAAC,CAUpF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const IMMERSIVE_VR = 'immersive-vr';
|
|
2
|
+
/**
|
|
3
|
+
* Whether this browser can start a WebXR immersive-vr session.
|
|
4
|
+
* Headset browsers resolve true; ordinary desktop Chrome resolves false.
|
|
5
|
+
*/
|
|
6
|
+
export async function immersiveVrSupported() {
|
|
7
|
+
const xr = navigator.xr;
|
|
8
|
+
if (!xr?.isSessionSupported)
|
|
9
|
+
return false;
|
|
10
|
+
try {
|
|
11
|
+
return await xr.isSessionSupported(IMMERSIVE_VR);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ask the browser for an immersive VR session and hand it to Three.js.
|
|
19
|
+
* Must run from a user gesture (the Enter VR button).
|
|
20
|
+
*/
|
|
21
|
+
export async function requestImmersiveVr(renderer) {
|
|
22
|
+
const xr = navigator.xr;
|
|
23
|
+
if (!xr?.requestSession)
|
|
24
|
+
throw new Error('WebXR is not available');
|
|
25
|
+
renderer.xr.enabled = true;
|
|
26
|
+
renderer.xr.setReferenceSpaceType('local-floor');
|
|
27
|
+
const session = await xr.requestSession(IMMERSIVE_VR, {
|
|
28
|
+
optionalFeatures: ['local-floor', 'bounded-floor'],
|
|
29
|
+
});
|
|
30
|
+
await renderer.xr.setSession(session);
|
|
31
|
+
return session;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/xr/session.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,EAAE,GAAG,SAAS,CAAC,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,EAAE,kBAAkB;QAAE,OAAO,KAAK,CAAC;IAC1C,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,QAAuB;IAC9D,MAAM,EAAE,GAAG,SAAS,CAAC,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,EAAE,cAAc;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IACnE,QAAQ,CAAC,EAAE,CAAC,OAAO,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,CAAC,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,cAAc,CAAC,YAAY,EAAE;QACpD,gBAAgB,EAAE,CAAC,aAAa,EAAE,eAAe,CAAC;KACnD,CAAC,CAAC;IACH,MAAM,QAAQ,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** Metres walked between two footsteps. */
|
|
2
|
+
const STRIDE = 1.6;
|
|
3
|
+
/** Below this ground speed (m/s) the player is standing, not walking. */
|
|
4
|
+
const MIN_SPEED = 0.6;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Footstep sounds for the local player, synthesised with Web Audio so no
|
|
8
|
+
* world has to ship a sound file. Steps follow distance walked, so sprinting
|
|
9
|
+
* quickens them and crouching slows them without any extra tuning.
|
|
10
|
+
*/
|
|
11
|
+
/** One audio context and noise buffer for every walker: browsers allow only a few contexts per page. */
|
|
12
|
+
let shared: { context: AudioContext; noise: AudioBuffer } | null = null;
|
|
13
|
+
|
|
14
|
+
export class Footsteps {
|
|
15
|
+
/**
|
|
16
|
+
* How loud this walker is where the listener stands: 1 for the player's own
|
|
17
|
+
* feet, less for someone further off, 0 for out of earshot.
|
|
18
|
+
*/
|
|
19
|
+
loudness = 1;
|
|
20
|
+
private walked = 0;
|
|
21
|
+
private left = false;
|
|
22
|
+
private wasGrounded = true;
|
|
23
|
+
private lastVertical = 0;
|
|
24
|
+
/** Fastest fall since leaving the ground, so landings sound as hard as they were. */
|
|
25
|
+
private fallSpeed = 0;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Call once per frame with the horizontal speed and whether feet are on the
|
|
29
|
+
* ground. `footfall` says whether the avatar's foot landed this frame; when
|
|
30
|
+
* the avatar's animation cannot tell (null), steps fall back to distance.
|
|
31
|
+
*/
|
|
32
|
+
update(
|
|
33
|
+
dt: number,
|
|
34
|
+
speed: number,
|
|
35
|
+
grounded: boolean,
|
|
36
|
+
footfall: boolean | null = null,
|
|
37
|
+
vertical = 0,
|
|
38
|
+
flying = false,
|
|
39
|
+
): void {
|
|
40
|
+
if (!flying) this.updateAir(grounded, vertical);
|
|
41
|
+
this.wasGrounded = grounded;
|
|
42
|
+
this.lastVertical = vertical;
|
|
43
|
+
if (footfall !== null) {
|
|
44
|
+
if (footfall && grounded && speed >= MIN_SPEED) this.play(Math.min(1, speed / 9));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!grounded || speed < MIN_SPEED) {
|
|
48
|
+
// Start the next walk half a stride in, so the first step comes quickly.
|
|
49
|
+
this.walked = STRIDE * 0.5;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.walked += speed * dt;
|
|
53
|
+
if (this.walked < STRIDE) return;
|
|
54
|
+
this.walked -= STRIDE * (0.92 + Math.random() * 0.16);
|
|
55
|
+
this.play(Math.min(1, speed / 9));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Jumps, double jumps and landings. */
|
|
59
|
+
private updateAir(grounded: boolean, vertical: number): void {
|
|
60
|
+
if (grounded) {
|
|
61
|
+
if (!this.wasGrounded && this.fallSpeed > 2) this.land(Math.min(1, this.fallSpeed / 20));
|
|
62
|
+
this.fallSpeed = 0;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.fallSpeed = Math.max(this.fallSpeed, -vertical);
|
|
66
|
+
const jumped = this.wasGrounded ? vertical > 1 : vertical - this.lastVertical > 3;
|
|
67
|
+
if (jumped) {
|
|
68
|
+
this.jump();
|
|
69
|
+
this.fallSpeed = 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Pushing off: a quick scuff, then a soft rush of air. Every part varies, like the steps. */
|
|
74
|
+
private jump(): void {
|
|
75
|
+
const jitter = (spread: number) => 1 + (Math.random() * 2 - 1) * spread;
|
|
76
|
+
this.play(0.6, { volume: 0.9 * jitter(0.3), decay: 0.7 * jitter(0.3), tone: 1.3 * jitter(0.2) });
|
|
77
|
+
const audio = this.audio();
|
|
78
|
+
if (!audio) return;
|
|
79
|
+
const { context, noise } = audio;
|
|
80
|
+
const now = context.currentTime + Math.random() * 0.02;
|
|
81
|
+
const length = 0.26 * jitter(0.3);
|
|
82
|
+
const source = context.createBufferSource();
|
|
83
|
+
source.buffer = noise;
|
|
84
|
+
source.playbackRate.value = jitter(0.2);
|
|
85
|
+
const filter = context.createBiquadFilter();
|
|
86
|
+
filter.type = 'bandpass';
|
|
87
|
+
filter.Q.value = 0.8 * jitter(0.4);
|
|
88
|
+
filter.frequency.setValueAtTime(500 * jitter(0.25), now);
|
|
89
|
+
filter.frequency.exponentialRampToValueAtTime(1400 * jitter(0.3), now + length * 0.85);
|
|
90
|
+
const gain = context.createGain();
|
|
91
|
+
gain.gain.setValueAtTime(0.0001, now);
|
|
92
|
+
gain.gain.exponentialRampToValueAtTime(0.034 * jitter(0.35) * this.loudness, now + 0.06 * jitter(0.4));
|
|
93
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, now + length);
|
|
94
|
+
source.connect(filter).connect(gain).connect(context.destination);
|
|
95
|
+
source.start(now, Math.random() * (noise.duration - length - 0.05));
|
|
96
|
+
source.stop(now + length + 0.02);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Both feet coming down, heavier and longer the harder the fall. Usually one
|
|
101
|
+
* lands a moment before the other, by a different amount each time.
|
|
102
|
+
*/
|
|
103
|
+
private land(hardness: number): void {
|
|
104
|
+
const jitter = (spread: number) => 1 + (Math.random() * 2 - 1) * spread;
|
|
105
|
+
const shape = () => ({
|
|
106
|
+
volume: (1.4 + hardness) * jitter(0.25),
|
|
107
|
+
decay: (1.4 + hardness * 0.6) * jitter(0.25),
|
|
108
|
+
tone: 0.8 * jitter(0.2),
|
|
109
|
+
});
|
|
110
|
+
this.play(0.4 + hardness * 0.6, shape());
|
|
111
|
+
if (Math.random() < 0.7) {
|
|
112
|
+
const second = shape();
|
|
113
|
+
second.volume *= 0.6;
|
|
114
|
+
window.setTimeout(() => this.play(0.4 + hardness * 0.6, second), 15 + Math.random() * 45);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A soft scuff with a faint low thump: a step on firm ground. Every part is
|
|
120
|
+
* jittered, and left and right feet differ slightly, so no two sound alike.
|
|
121
|
+
*/
|
|
122
|
+
private play(intensity: number, shape: { volume?: number; decay?: number; tone?: number } = {}): void {
|
|
123
|
+
const { volume: loudness = 1, decay: length = 1, tone = 1 } = shape;
|
|
124
|
+
const audio = this.audio();
|
|
125
|
+
if (!audio) return;
|
|
126
|
+
const { context, noise } = audio;
|
|
127
|
+
const jitter = (spread: number) => 1 + (Math.random() * 2 - 1) * spread;
|
|
128
|
+
this.left = !this.left;
|
|
129
|
+
const now = context.currentTime + Math.random() * 0.025;
|
|
130
|
+
const volume = (0.065 + intensity * 0.065) * jitter(0.3) * (this.left ? 1 : 0.85) * loudness * this.loudness;
|
|
131
|
+
const decay = 0.09 * jitter(0.35) * length;
|
|
132
|
+
|
|
133
|
+
const source = context.createBufferSource();
|
|
134
|
+
source.buffer = noise;
|
|
135
|
+
source.playbackRate.value = jitter(0.25);
|
|
136
|
+
const filter = context.createBiquadFilter();
|
|
137
|
+
filter.type = 'lowpass';
|
|
138
|
+
filter.frequency.value = (this.left ? 520 : 440) * jitter(0.3) * tone;
|
|
139
|
+
filter.Q.value = 0.4 + Math.random() * 0.6;
|
|
140
|
+
const gain = context.createGain();
|
|
141
|
+
// A few milliseconds of attack takes the click off the start.
|
|
142
|
+
gain.gain.setValueAtTime(0.0001, now);
|
|
143
|
+
gain.gain.exponentialRampToValueAtTime(volume, now + 0.008 * jitter(0.5));
|
|
144
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, now + decay);
|
|
145
|
+
// Random offset into the noise, so each step starts on different grain.
|
|
146
|
+
const offset = Math.random() * (noise.duration - decay - 0.02);
|
|
147
|
+
source.connect(filter).connect(gain).connect(context.destination);
|
|
148
|
+
source.start(now, Math.max(0, offset));
|
|
149
|
+
source.stop(now + decay + 0.02);
|
|
150
|
+
|
|
151
|
+
const thump = context.createOscillator();
|
|
152
|
+
const pitch = 85 * jitter(0.2) * Math.min(1, tone);
|
|
153
|
+
thump.frequency.setValueAtTime(pitch, now);
|
|
154
|
+
thump.frequency.exponentialRampToValueAtTime(pitch * 0.5, now + 0.07 * length);
|
|
155
|
+
const thumpGain = context.createGain();
|
|
156
|
+
thumpGain.gain.setValueAtTime(0.0001, now);
|
|
157
|
+
thumpGain.gain.exponentialRampToValueAtTime(volume * 0.5 * jitter(0.4), now + 0.01);
|
|
158
|
+
thumpGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.08 * length);
|
|
159
|
+
thump.connect(thumpGain).connect(context.destination);
|
|
160
|
+
thump.start(now);
|
|
161
|
+
thump.stop(now + 0.09 * length + 0.01);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The shared audio, or null when there is none, it is not running yet, or this walker is out of earshot. */
|
|
165
|
+
private audio(): { context: AudioContext; noise: AudioBuffer } | null {
|
|
166
|
+
if (this.loudness < 0.02) return null;
|
|
167
|
+
if (!shared) {
|
|
168
|
+
if (typeof AudioContext === 'undefined') return null;
|
|
169
|
+
const context = new AudioContext();
|
|
170
|
+
const length = Math.floor(context.sampleRate * 0.6);
|
|
171
|
+
const noise = context.createBuffer(1, length, context.sampleRate);
|
|
172
|
+
const data = noise.getChannelData(0);
|
|
173
|
+
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
|
|
174
|
+
shared = { context, noise };
|
|
175
|
+
}
|
|
176
|
+
// Browsers start audio suspended until the page has had a click or key press.
|
|
177
|
+
if (shared.context.state === 'suspended') void shared.context.resume().catch(() => undefined);
|
|
178
|
+
return shared.context.state === 'running' ? shared : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
dispose(): void {
|
|
182
|
+
// The shared context stays for the other walkers; nothing of this one's is left playing.
|
|
183
|
+
this.loudness = 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/avatar/descriptor.ts
CHANGED
|
@@ -21,6 +21,32 @@ export interface AvatarDescriptor {
|
|
|
21
21
|
expiresAt: string | null;
|
|
22
22
|
/** The avatar's public page, for credit where its license asks for it. */
|
|
23
23
|
sourceUrl: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* The model link signed by the WorldMesh hub for showing to other players,
|
|
26
|
+
* or null when it may not be shown to them (see avatar/share.ts).
|
|
27
|
+
*/
|
|
28
|
+
share?: SignedAvatarShare | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A model link the hub vouched for: `u` link, `f` format, `i` stable id, `x` expiry (unix seconds), `s` Ed25519 signature. */
|
|
32
|
+
export interface SignedAvatarShare {
|
|
33
|
+
u: string;
|
|
34
|
+
f: AvatarFormat;
|
|
35
|
+
i: string;
|
|
36
|
+
x: number;
|
|
37
|
+
s: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Accepts a signed share from the network only if it has the right shape. Says nothing about the signature. */
|
|
41
|
+
export function parseAvatarShare(value: unknown): SignedAvatarShare | null {
|
|
42
|
+
if (!value || typeof value !== 'object') return null;
|
|
43
|
+
const v = value as Record<string, unknown>;
|
|
44
|
+
if (typeof v.u !== 'string' || !v.u.startsWith('https:') || v.u.length > 1800 || !isLoadableUrl(v.u)) return null;
|
|
45
|
+
if (typeof v.f !== 'string' || !FORMATS.includes(v.f)) return null;
|
|
46
|
+
if (typeof v.i !== 'string' || !/^[a-z0-9]{1,16}$/.test(v.i)) return null;
|
|
47
|
+
if (typeof v.x !== 'number' || !Number.isInteger(v.x) || v.x <= 0 || v.x > 1e11) return null;
|
|
48
|
+
if (typeof v.s !== 'string' || !/^[A-Za-z0-9_-]{86}$/.test(v.s)) return null;
|
|
49
|
+
return { u: v.u, f: v.f as AvatarFormat, i: v.i, x: v.x, s: v.s };
|
|
24
50
|
}
|
|
25
51
|
|
|
26
52
|
const FORMATS: readonly string[] = ['vrm', 'glb', 'gltf'];
|
|
@@ -52,5 +78,7 @@ export function parseAvatarDescriptor(value: unknown): AvatarDescriptor | null {
|
|
|
52
78
|
modelUrl: d.modelUrl,
|
|
53
79
|
expiresAt: text(d.expiresAt),
|
|
54
80
|
sourceUrl: isLoadableUrl(d.sourceUrl) ? d.sourceUrl : null,
|
|
81
|
+
// Absent (an older hub) is not the same as null (the hub says: do not show to others).
|
|
82
|
+
share: 'share' in d ? parseAvatarShare(d.share) : undefined,
|
|
55
83
|
};
|
|
56
84
|
}
|
package/src/avatar/loader.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnimationMixer, Box3, Group, MathUtils, Mesh, Texture, type AnimationAction, type Object3D } from 'three';
|
|
2
|
-
import type
|
|
2
|
+
import { attachAvatarAppear, type AvatarMotion } from '../player/avatar.js';
|
|
3
3
|
import type { AvatarDescriptor } from './descriptor.js';
|
|
4
4
|
|
|
5
5
|
/** A loaded external avatar, normalised to the WorldMesh body convention. */
|
|
@@ -17,6 +17,19 @@ export interface LoadAvatarOptions {
|
|
|
17
17
|
/** Refuse models larger than this. Defaults to 40 MB. */
|
|
18
18
|
maxBytes?: number;
|
|
19
19
|
signal?: AbortSignal;
|
|
20
|
+
/** The model file, already downloaded (downloadAvatarModel): nothing is fetched again. */
|
|
21
|
+
data?: ArrayBuffer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_MAX_BYTES = 40 * 1024 * 1024;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Just the download: the model file the descriptor points at, within the size
|
|
28
|
+
* limit. Lets a page fetch it once and hand the same bytes to loadAvatarModel
|
|
29
|
+
* and on to an embedded world.
|
|
30
|
+
*/
|
|
31
|
+
export function downloadAvatarModel(descriptor: AvatarDescriptor, options: { maxBytes?: number; signal?: AbortSignal } = {}): Promise<ArrayBuffer> {
|
|
32
|
+
return download(descriptor.modelUrl, options.maxBytes ?? DEFAULT_MAX_BYTES, options.signal);
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
/**
|
|
@@ -26,7 +39,9 @@ export interface LoadAvatarOptions {
|
|
|
26
39
|
* never download them.
|
|
27
40
|
*/
|
|
28
41
|
export async function loadAvatarModel(descriptor: AvatarDescriptor, options: LoadAvatarOptions): Promise<LoadedAvatar> {
|
|
29
|
-
const
|
|
42
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
43
|
+
if (options.data && options.data.byteLength > maxBytes) throw new Error('Model is too large');
|
|
44
|
+
const buffer = options.data ?? (await download(descriptor.modelUrl, maxBytes, options.signal));
|
|
30
45
|
const [{ GLTFLoader }, vrmModule] = await Promise.all([
|
|
31
46
|
import('three/examples/jsm/loaders/GLTFLoader.js'),
|
|
32
47
|
descriptor.format === 'vrm' ? import('@pixiv/three-vrm') : Promise.resolve(null),
|
|
@@ -49,6 +64,8 @@ export async function loadAvatarModel(descriptor: AvatarDescriptor, options: Loa
|
|
|
49
64
|
child.frustumCulled = false;
|
|
50
65
|
if ((child as Mesh).isMesh) child.castShadow = true;
|
|
51
66
|
});
|
|
67
|
+
// Warps in like the default body when a page brings it in (setAvatarAppear).
|
|
68
|
+
attachAvatarAppear(model);
|
|
52
69
|
|
|
53
70
|
// glTF and VRM face +Z; WorldMesh bodies face -Z. Scale to the player and
|
|
54
71
|
// stand the feet on y = 0.
|