@opsydyn/effect-three 0.0.0-stage → 0.1.0
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/CHANGELOG.md +58 -0
- package/LICENSE +21 -0
- package/README.md +191 -2
- package/dist/index-4045wme0.js +129 -0
- package/dist/index-4045wme0.js.map +10 -0
- package/dist/index-ef8pbv9g.js +5118 -0
- package/dist/index-ef8pbv9g.js.map +40 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1361 -0
- package/dist/index.js.map +14 -0
- package/dist/kernel/animation.d.ts +14 -0
- package/dist/kernel/animation.d.ts.map +1 -0
- package/dist/kernel/behaviours.d.ts +16 -0
- package/dist/kernel/behaviours.d.ts.map +1 -0
- package/dist/kernel/bodies.d.ts +60 -0
- package/dist/kernel/bodies.d.ts.map +1 -0
- package/dist/kernel/bodySources.d.ts +47 -0
- package/dist/kernel/bodySources.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +564 -0
- package/dist/kernel/build.d.ts.map +1 -0
- package/dist/kernel/cameraMotion.d.ts +39 -0
- package/dist/kernel/cameraMotion.d.ts.map +1 -0
- package/dist/kernel/clock.d.ts +17 -0
- package/dist/kernel/clock.d.ts.map +1 -0
- package/dist/kernel/contactShadowPasses.d.ts +47 -0
- package/dist/kernel/contactShadowPasses.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +1120 -0
- package/dist/kernel/description.d.ts.map +1 -0
- package/dist/kernel/diff.d.ts +90 -0
- package/dist/kernel/diff.d.ts.map +1 -0
- package/dist/kernel/environmentPresets.d.ts +30 -0
- package/dist/kernel/environmentPresets.d.ts.map +1 -0
- package/dist/kernel/equality.d.ts +64 -0
- package/dist/kernel/equality.d.ts.map +1 -0
- package/dist/kernel/frames.d.ts +55 -0
- package/dist/kernel/frames.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +37 -0
- package/dist/kernel/index.d.ts.map +1 -0
- package/dist/kernel/index.js +335 -0
- package/dist/kernel/index.js.map +9 -0
- package/dist/kernel/instances.d.ts +26 -0
- package/dist/kernel/instances.d.ts.map +1 -0
- package/dist/kernel/labels.d.ts +45 -0
- package/dist/kernel/labels.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts +49 -0
- package/dist/kernel/loop.d.ts.map +1 -0
- package/dist/kernel/materials.d.ts +24 -0
- package/dist/kernel/materials.d.ts.map +1 -0
- package/dist/kernel/motion.d.ts +36 -0
- package/dist/kernel/motion.d.ts.map +1 -0
- package/dist/kernel/orbit.d.ts +127 -0
- package/dist/kernel/orbit.d.ts.map +1 -0
- package/dist/kernel/overlays.d.ts +16 -0
- package/dist/kernel/overlays.d.ts.map +1 -0
- package/dist/kernel/performance.d.ts +41 -0
- package/dist/kernel/performance.d.ts.map +1 -0
- package/dist/kernel/physics.d.ts +166 -0
- package/dist/kernel/physics.d.ts.map +1 -0
- package/dist/kernel/physicsDebug.d.ts +18 -0
- package/dist/kernel/physicsDebug.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +357 -0
- package/dist/kernel/pointer.d.ts.map +1 -0
- package/dist/kernel/renderKit.d.ts +48 -0
- package/dist/kernel/renderKit.d.ts.map +1 -0
- package/dist/kernel/renderer.d.ts +83 -0
- package/dist/kernel/renderer.d.ts.map +1 -0
- package/dist/kernel/resources.d.ts +112 -0
- package/dist/kernel/resources.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +155 -0
- package/dist/kernel/sceneGraph.d.ts.map +1 -0
- package/dist/kernel/sceneIndex.d.ts +90 -0
- package/dist/kernel/sceneIndex.d.ts.map +1 -0
- package/dist/kernel/stage.d.ts +87 -0
- package/dist/kernel/stage.d.ts.map +1 -0
- package/dist/kernel/textLayout.d.ts +29 -0
- package/dist/kernel/textLayout.d.ts.map +1 -0
- package/dist/kernel/tween.d.ts +35 -0
- package/dist/kernel/tween.d.ts.map +1 -0
- package/dist/kernel/xrInput.d.ts +107 -0
- package/dist/kernel/xrInput.d.ts.map +1 -0
- package/dist/kernel/xrLocomotion.d.ts +38 -0
- package/dist/kernel/xrLocomotion.d.ts.map +1 -0
- package/dist/runtime/FrameSource.d.ts +29 -0
- package/dist/runtime/FrameSource.d.ts.map +1 -0
- package/dist/runtime/ResourceRegistry.d.ts +18 -0
- package/dist/runtime/ResourceRegistry.d.ts.map +1 -0
- package/dist/runtime/SceneRuntime.d.ts +68 -0
- package/dist/runtime/SceneRuntime.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +2536 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -0
- package/dist/runtime/errors.d.ts +19 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/webgl/contactShadows.d.ts +5 -0
- package/dist/webgl/contactShadows.d.ts.map +1 -0
- package/dist/webgl/index.d.ts +11 -0
- package/dist/webgl/index.d.ts.map +1 -0
- package/dist/webgl/index.js +239 -0
- package/dist/webgl/index.js.map +13 -0
- package/dist/webgl/shaderGrid.d.ts +27 -0
- package/dist/webgl/shaderGrid.d.ts.map +1 -0
- package/dist/webgl/sky.d.ts +4 -0
- package/dist/webgl/sky.d.ts.map +1 -0
- package/dist/webgpu/contactShadows.d.ts +5 -0
- package/dist/webgpu/contactShadows.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts +10 -0
- package/dist/webgpu/index.d.ts.map +1 -0
- package/dist/webgpu/index.js +216 -0
- package/dist/webgpu/index.js.map +13 -0
- package/dist/webgpu/shaderGrid.d.ts +5 -0
- package/dist/webgpu/shaderGrid.d.ts.map +1 -0
- package/dist/webgpu/sky.d.ts +4 -0
- package/dist/webgpu/sky.d.ts.map +1 -0
- package/package.json +82 -4
- package/src/index.ts +11 -0
- package/src/kernel/animation.ts +24 -0
- package/src/kernel/behaviours.ts +36 -0
- package/src/kernel/bodies.ts +338 -0
- package/src/kernel/bodySources.ts +203 -0
- package/src/kernel/build.ts +1325 -0
- package/src/kernel/cameraMotion.ts +202 -0
- package/src/kernel/clock.ts +35 -0
- package/src/kernel/contactShadowPasses.ts +207 -0
- package/src/kernel/description.ts +1197 -0
- package/src/kernel/diff.ts +383 -0
- package/src/kernel/environmentPresets.ts +38 -0
- package/src/kernel/equality.ts +604 -0
- package/src/kernel/frames.ts +124 -0
- package/src/kernel/index.ts +274 -0
- package/src/kernel/instances.ts +97 -0
- package/src/kernel/labels.ts +48 -0
- package/src/kernel/loop.ts +248 -0
- package/src/kernel/materials.ts +258 -0
- package/src/kernel/motion.ts +75 -0
- package/src/kernel/orbit.ts +554 -0
- package/src/kernel/overlays.ts +130 -0
- package/src/kernel/performance.ts +101 -0
- package/src/kernel/physics.ts +361 -0
- package/src/kernel/physicsDebug.ts +68 -0
- package/src/kernel/pointer.ts +785 -0
- package/src/kernel/renderKit.ts +53 -0
- package/src/kernel/renderer.ts +136 -0
- package/src/kernel/resources.ts +504 -0
- package/src/kernel/sceneGraph.ts +1440 -0
- package/src/kernel/sceneIndex.ts +219 -0
- package/src/kernel/stage.ts +157 -0
- package/src/kernel/textLayout.ts +102 -0
- package/src/kernel/tween.ts +80 -0
- package/src/kernel/xrInput.ts +230 -0
- package/src/kernel/xrLocomotion.ts +72 -0
- package/src/runtime/FrameSource.ts +41 -0
- package/src/runtime/ResourceRegistry.ts +24 -0
- package/src/runtime/SceneRuntime.ts +229 -0
- package/src/runtime/SceneSchema.ts +983 -0
- package/src/runtime/errors.ts +18 -0
- package/src/runtime/index.ts +10 -0
- package/src/webgl/contactShadows.ts +67 -0
- package/src/webgl/index.ts +19 -0
- package/src/webgl/shaderGrid.ts +196 -0
- package/src/webgl/sky.ts +42 -0
- package/src/webgpu/contactShadows.ts +83 -0
- package/src/webgpu/index.ts +19 -0
- package/src/webgpu/shaderGrid.ts +158 -0
- package/src/webgpu/sky.ts +35 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XR input as the scene's events: controllers and hands pointing, selecting, dragging,
|
|
3
|
+
* pushing sticks and pressing buttons, and (in AR) selecting a real surface.
|
|
4
|
+
*
|
|
5
|
+
* Behavioural reference: pmndrs/xr at `0e1acd8`. Its pointers ray-cast along each input
|
|
6
|
+
* source's target ray every frame (`packages/xr/src/pointer`), entering and leaving what
|
|
7
|
+
* they point at, capturing the object a select starts on so moving drags it, and turning
|
|
8
|
+
* select start and end into pointer down, up and click. Its controller locomotion
|
|
9
|
+
* (`controller-locomotion.ts`) reads the thumbstick each frame; here a stick reports when
|
|
10
|
+
* it is pushed past halfway in a direction and when it comes back, so a Model can snap
|
|
11
|
+
* turn and teleport by changing `xr({ origin, heading })`.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { Vec3 } from './description.ts';
|
|
16
|
+
import type { Hit, Picker, Ray, SceneEvent } from './pointer.ts';
|
|
17
|
+
|
|
18
|
+
/** Which hand an input source is held in or is, as WebXR's `handedness`. */
|
|
19
|
+
export type XrHand = 'left' | 'right' | 'none';
|
|
20
|
+
|
|
21
|
+
/** Where a thumbstick is pushed: `forward` is away from the player. */
|
|
22
|
+
export type XrStickDirection = 'center' | 'left' | 'right' | 'forward' | 'back';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A controller's buttons, by the `xr-standard` gamepad mapping: the trigger, the grip
|
|
26
|
+
* (`squeeze`), a touchpad, the thumbstick pressed, and the face buttons (A and B on the
|
|
27
|
+
* right controller, X and Y on the left: `primary` and `secondary`).
|
|
28
|
+
*/
|
|
29
|
+
export type XrButtonName =
|
|
30
|
+
| 'trigger'
|
|
31
|
+
| 'squeeze'
|
|
32
|
+
| 'touchpad'
|
|
33
|
+
| 'thumbstick'
|
|
34
|
+
| 'primary'
|
|
35
|
+
| 'secondary';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A plane the session detected (`xr({ planes: true })`): a floor, wall or table, in the
|
|
39
|
+
* scene's space. `polygon` is its outline in its own space (on its x–z plane); `label` is
|
|
40
|
+
* what the device took it for (`floor`, `wall`, `table`…), or `''` when it did not say.
|
|
41
|
+
* Follow one with `group({ follow: planeSpace(id) })`.
|
|
42
|
+
*/
|
|
43
|
+
export type XrPlane = {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly orientation: 'horizontal' | 'vertical';
|
|
46
|
+
readonly label: string;
|
|
47
|
+
readonly position: Vec3;
|
|
48
|
+
readonly rotation: Vec3;
|
|
49
|
+
readonly polygon: ReadonlyArray<Vec3>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A mesh the session detected (`xr({ meshes: true })`): furniture, a wall, the room. Its
|
|
54
|
+
* pose is in the scene's space; its geometry is lent for `xrMesh(id)` rather than sent.
|
|
55
|
+
*/
|
|
56
|
+
export type XrMesh = {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly label: string;
|
|
59
|
+
readonly position: Vec3;
|
|
60
|
+
readonly rotation: Vec3;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Input from an immersive session, each from an input source (`source` tells them apart). */
|
|
64
|
+
export type XrInput =
|
|
65
|
+
/** A select (the trigger, or a pinch) beginning or ending, along the source's ray. */
|
|
66
|
+
| { readonly _tag: 'SelectStart' | 'SelectEnd'; readonly source: number; readonly ray: Ray }
|
|
67
|
+
/** Where the source points this frame. */
|
|
68
|
+
| { readonly _tag: 'Aim'; readonly source: number; readonly ray: Ray }
|
|
69
|
+
/** The source went away (put down, or the session ended). */
|
|
70
|
+
| { readonly _tag: 'Lost'; readonly source: number }
|
|
71
|
+
| {
|
|
72
|
+
readonly _tag: 'Stick';
|
|
73
|
+
readonly source: number;
|
|
74
|
+
readonly hand: XrHand;
|
|
75
|
+
readonly direction: XrStickDirection;
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
readonly _tag: 'Button';
|
|
79
|
+
readonly source: number;
|
|
80
|
+
readonly hand: XrHand;
|
|
81
|
+
readonly button: XrButtonName;
|
|
82
|
+
readonly pressed: boolean;
|
|
83
|
+
}
|
|
84
|
+
/** Gliding (`xr({ glide })`) stopped: where the player stands now, for the Model. */
|
|
85
|
+
| { readonly _tag: 'Moved'; readonly origin: Vec3; readonly heading: number }
|
|
86
|
+
/** The detected planes changed: all of them, as they are now. */
|
|
87
|
+
| { readonly _tag: 'Planes'; readonly planes: ReadonlyArray<XrPlane> }
|
|
88
|
+
/** The detected meshes changed: all of them, as they are now. */
|
|
89
|
+
| { readonly _tag: 'Meshes'; readonly meshes: ReadonlyArray<XrMesh> }
|
|
90
|
+
/** In AR, a select whose ray met a real surface: where, and the surface's normal. */
|
|
91
|
+
| {
|
|
92
|
+
readonly _tag: 'Surface';
|
|
93
|
+
readonly source: number;
|
|
94
|
+
readonly hand: XrHand;
|
|
95
|
+
readonly point: Vec3;
|
|
96
|
+
readonly normal: Vec3;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** One source's state: what it points at, the select it holds, and its last ray. */
|
|
100
|
+
type Source = {
|
|
101
|
+
hovered: string | undefined;
|
|
102
|
+
/** The key a held select started on (`undefined` for nothing) and its ray, while held. */
|
|
103
|
+
pressed: { readonly key: string | undefined; readonly ray: Ray } | undefined;
|
|
104
|
+
dragged: boolean;
|
|
105
|
+
ray: Ray | undefined;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const none: ReadonlyArray<SceneEvent> = [];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* How far a held source must move before it drags, as the mouse must travel a few pixels:
|
|
112
|
+
* its ray turned by a degree, or moved a centimetre. Hands and controllers never hold
|
|
113
|
+
* perfectly still, and a select that does not move is a click.
|
|
114
|
+
*/
|
|
115
|
+
const dragStart = { turn: Math.cos((1 * Math.PI) / 180), shift: 0.01 } as const;
|
|
116
|
+
|
|
117
|
+
const moved = (from: Ray, to: Ray): boolean => {
|
|
118
|
+
const [a, b, c] = from.direction;
|
|
119
|
+
const [x, y, z] = to.direction;
|
|
120
|
+
const [p, q, r] = from.origin;
|
|
121
|
+
const [u, v, w] = to.origin;
|
|
122
|
+
return (
|
|
123
|
+
a * x + b * y + c * z < dragStart.turn || Math.hypot(u - p, v - q, w - r) > dragStart.shift
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Creates the tracker turning one session's XR input into scene events. */
|
|
128
|
+
export const createXrTracker = (picker: Picker) => {
|
|
129
|
+
const sources = new Map<number, Source>();
|
|
130
|
+
|
|
131
|
+
const sourceOf = (id: number): Source => {
|
|
132
|
+
const known = sources.get(id);
|
|
133
|
+
if (known !== undefined) return known;
|
|
134
|
+
const created: Source = {
|
|
135
|
+
hovered: undefined,
|
|
136
|
+
pressed: undefined,
|
|
137
|
+
dragged: false,
|
|
138
|
+
ray: undefined,
|
|
139
|
+
};
|
|
140
|
+
sources.set(id, created);
|
|
141
|
+
return created;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Leaves and enters as what the source points at changes. */
|
|
145
|
+
const hover = (source: Source, hit: Hit | undefined, events: Array<SceneEvent>): void => {
|
|
146
|
+
const next = hit?.key;
|
|
147
|
+
if (next === source.hovered) return;
|
|
148
|
+
if (source.hovered !== undefined) events.push({ _tag: 'PointerLeave', key: source.hovered });
|
|
149
|
+
if (hit !== undefined) events.push({ _tag: 'PointerEnter', ...hit });
|
|
150
|
+
source.hovered = next;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const aim = (source: Source, ray: Ray): ReadonlyArray<SceneEvent> => {
|
|
154
|
+
source.ray = ray;
|
|
155
|
+
const events: Array<SceneEvent> = [];
|
|
156
|
+
const pressed = source.pressed;
|
|
157
|
+
// A select held on a mesh captures it: moving drags it, wherever the ray now points.
|
|
158
|
+
if (pressed?.key !== undefined && (source.dragged || moved(pressed.ray, ray))) {
|
|
159
|
+
source.dragged = true;
|
|
160
|
+
events.push({ _tag: 'Drag', key: pressed.key, button: 0, ray });
|
|
161
|
+
}
|
|
162
|
+
hover(source, picker.pickRay(ray), events);
|
|
163
|
+
return events;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const selectStart = (source: Source, ray: Ray): ReadonlyArray<SceneEvent> => {
|
|
167
|
+
const hit = picker.pickRay(ray);
|
|
168
|
+
source.pressed = { key: hit?.key, ray };
|
|
169
|
+
source.dragged = false;
|
|
170
|
+
return hit === undefined ? none : [{ _tag: 'PointerDown', button: 0, ...hit }];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const selectEnd = (source: Source, ray: Ray): ReadonlyArray<SceneEvent> => {
|
|
174
|
+
const from = source.pressed?.key;
|
|
175
|
+
const dragged = source.dragged;
|
|
176
|
+
source.pressed = undefined;
|
|
177
|
+
source.dragged = false;
|
|
178
|
+
const events: Array<SceneEvent> = [];
|
|
179
|
+
if (from !== undefined && dragged) events.push({ _tag: 'DragEnd', key: from, button: 0, ray });
|
|
180
|
+
const hit = picker.pickRay(ray);
|
|
181
|
+
if (hit === undefined) {
|
|
182
|
+
if (from === undefined) events.push({ _tag: 'PointerMissed', button: 0 });
|
|
183
|
+
return events;
|
|
184
|
+
}
|
|
185
|
+
events.push({ _tag: 'PointerUp', button: 0, ...hit });
|
|
186
|
+
if (from === hit.key) events.push({ _tag: 'Click', button: 0, ...hit });
|
|
187
|
+
return events;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** A source gone: it leaves what it pointed at, and a drag ends where it last aimed. */
|
|
191
|
+
const lost = (id: number): ReadonlyArray<SceneEvent> => {
|
|
192
|
+
const source = sources.get(id);
|
|
193
|
+
sources.delete(id);
|
|
194
|
+
if (source === undefined) return none;
|
|
195
|
+
const events: Array<SceneEvent> = [];
|
|
196
|
+
const held = source.pressed?.key;
|
|
197
|
+
if (held !== undefined && source.dragged && source.ray !== undefined) {
|
|
198
|
+
events.push({ _tag: 'DragEnd', key: held, button: 0, ray: source.ray });
|
|
199
|
+
}
|
|
200
|
+
if (source.hovered !== undefined) events.push({ _tag: 'PointerLeave', key: source.hovered });
|
|
201
|
+
return events;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return (input: XrInput): ReadonlyArray<SceneEvent> => {
|
|
205
|
+
switch (input._tag) {
|
|
206
|
+
case 'Aim':
|
|
207
|
+
return aim(sourceOf(input.source), input.ray);
|
|
208
|
+
case 'SelectStart':
|
|
209
|
+
return selectStart(sourceOf(input.source), input.ray);
|
|
210
|
+
case 'SelectEnd':
|
|
211
|
+
return selectEnd(sourceOf(input.source), input.ray);
|
|
212
|
+
case 'Lost':
|
|
213
|
+
return lost(input.source);
|
|
214
|
+
case 'Stick':
|
|
215
|
+
return [{ _tag: 'XrStick', hand: input.hand, direction: input.direction }];
|
|
216
|
+
case 'Button':
|
|
217
|
+
return [
|
|
218
|
+
{ _tag: 'XrButton', hand: input.hand, button: input.button, pressed: input.pressed },
|
|
219
|
+
];
|
|
220
|
+
case 'Moved':
|
|
221
|
+
return [{ _tag: 'XrMoved', origin: input.origin, heading: input.heading }];
|
|
222
|
+
case 'Planes':
|
|
223
|
+
return [{ _tag: 'XrPlanes', planes: input.planes }];
|
|
224
|
+
case 'Meshes':
|
|
225
|
+
return [{ _tag: 'XrMeshes', meshes: input.meshes }];
|
|
226
|
+
case 'Surface':
|
|
227
|
+
return [{ _tag: 'XrSurface', hand: input.hand, point: input.point, normal: input.normal }];
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving the player in an immersive session, as functions from the scene's `xr` settings
|
|
3
|
+
* to new ones: the Model keeps where the player stands, and the session follows it.
|
|
4
|
+
*
|
|
5
|
+
* Behavioural reference: pmndrs/xr at `0e1acd8`, `controller-locomotion.ts`: the right
|
|
6
|
+
* thumbstick snap-turns 45° once pushed past halfway (and must come back before turning
|
|
7
|
+
* again), and the left one moves the player along where they face. Here moving is a step
|
|
8
|
+
* per push, not a glide: sticks arrive as `XrStick` events when pushed and let back.
|
|
9
|
+
* `teleport` is `@react-three/xr`'s `TeleportTarget`: the player stands where they point.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { Vec3, Xr } from './description.ts';
|
|
14
|
+
import type { XrHand, XrStickDirection } from './xrInput.ts';
|
|
15
|
+
|
|
16
|
+
export type LocomotionOptions = {
|
|
17
|
+
/** Degrees a snap turn turns by. Default 45, as pmndrs/xr's. */
|
|
18
|
+
readonly snapDegrees?: number;
|
|
19
|
+
/** Metres a step moves. Default 1. */
|
|
20
|
+
readonly step?: number;
|
|
21
|
+
/** The hand whose stick turns; the other moves. Default `right`, as pmndrs/xr's. */
|
|
22
|
+
readonly turnHand?: Exclude<XrHand, 'none'>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** A step `forward` (`-1` back) and `right` (`-1` left) of the player facing `heading`. */
|
|
26
|
+
const stepped = (settings: Xr, forward: number, right: number): Vec3 => {
|
|
27
|
+
const [x, y, z] = settings.origin;
|
|
28
|
+
const sin = Math.sin(settings.heading);
|
|
29
|
+
const cos = Math.cos(settings.heading);
|
|
30
|
+
// Facing `heading` about +y from −z: forward is (−sin, −cos), right is (cos, −sin).
|
|
31
|
+
return [x - forward * sin + right * cos, y, z - forward * cos - right * sin];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The settings after a stick is pushed: the turning hand's left and right snap-turn, the
|
|
36
|
+
* other hand's stick steps forward, back, left or right. Centring changes nothing.
|
|
37
|
+
*/
|
|
38
|
+
export const locomote = (
|
|
39
|
+
settings: Xr,
|
|
40
|
+
stick: { readonly hand: XrHand; readonly direction: XrStickDirection },
|
|
41
|
+
options: LocomotionOptions = {},
|
|
42
|
+
): Xr => {
|
|
43
|
+
const turnHand = options.turnHand ?? 'right';
|
|
44
|
+
const step = options.step ?? 1;
|
|
45
|
+
const snap = ((options.snapDegrees ?? 45) * Math.PI) / 180;
|
|
46
|
+
if (stick.hand === turnHand) {
|
|
47
|
+
if (stick.direction === 'left') return { ...settings, heading: settings.heading + snap };
|
|
48
|
+
if (stick.direction === 'right') return { ...settings, heading: settings.heading - snap };
|
|
49
|
+
return settings;
|
|
50
|
+
}
|
|
51
|
+
const moves: Readonly<Record<XrStickDirection, readonly [number, number] | undefined>> = {
|
|
52
|
+
center: undefined,
|
|
53
|
+
forward: [step, 0],
|
|
54
|
+
back: [-step, 0],
|
|
55
|
+
left: [0, -step],
|
|
56
|
+
right: [0, step],
|
|
57
|
+
};
|
|
58
|
+
const move = moves[stick.direction];
|
|
59
|
+
return move === undefined ? settings : { ...settings, origin: stepped(settings, ...move) };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The settings with the player standing at `point` (on the floor there), facing as before. */
|
|
63
|
+
export const teleport = (settings: Xr, point: Vec3): Xr => ({
|
|
64
|
+
...settings,
|
|
65
|
+
origin: [point[0], settings.origin[1], point[2]],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** The settings where gliding left the player (an `XrMoved` event): its origin and heading. */
|
|
69
|
+
export const glided = (
|
|
70
|
+
settings: Xr,
|
|
71
|
+
moved: { readonly origin: Vec3; readonly heading: number },
|
|
72
|
+
): Xr => ({ ...settings, origin: moved.origin, heading: moved.heading });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where frames come from. Wraps a kernel `FrameScheduler` so a scene's per-frame callback
|
|
3
|
+
* is registered for exactly the lifetime of its scope.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Effect, Layer } from 'effect';
|
|
8
|
+
|
|
9
|
+
import { createGatedFrames, type FrameControl, type FrameScheduler } from '../kernel/index.ts';
|
|
10
|
+
|
|
11
|
+
const registration = (scheduler: FrameScheduler, onFrame: () => void) => () => {
|
|
12
|
+
const gate = createGatedFrames(scheduler);
|
|
13
|
+
const unsubscribe = gate.subscribe(onFrame);
|
|
14
|
+
const control: FrameControl = {
|
|
15
|
+
wake: () => gate.setOpen(true),
|
|
16
|
+
sleep: () => gate.setOpen(false),
|
|
17
|
+
};
|
|
18
|
+
return { control, unsubscribe };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The frame source service for `scheduler`, for providing directly with `provideService`. */
|
|
22
|
+
export const fromScheduler = (scheduler: FrameScheduler) => ({
|
|
23
|
+
/**
|
|
24
|
+
* Calls `onFrame` once per frame until the current scope closes, while awake. Returns
|
|
25
|
+
* the control that puts the subscription to sleep and wakes it.
|
|
26
|
+
*/
|
|
27
|
+
subscribe: (onFrame: () => void) => {
|
|
28
|
+
const register = Effect.sync(registration(scheduler, onFrame));
|
|
29
|
+
return Effect.acquireRelease(register, (registered) =>
|
|
30
|
+
Effect.sync(registered.unsubscribe),
|
|
31
|
+
).pipe(Effect.map((registered) => registered.control));
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export class FrameSource extends Context.Service<FrameSource, ReturnType<typeof fromScheduler>>()(
|
|
36
|
+
'effect-three/FrameSource',
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
/** Provides a frame source backed by `scheduler`. */
|
|
40
|
+
export const layer = (scheduler: FrameScheduler) =>
|
|
41
|
+
Layer.succeed(FrameSource, fromScheduler(scheduler));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-level owner of shared GPU resources (ADR-005). Every scene runtime built with the
|
|
3
|
+
* same Layer shares one kernel resource store; the store is disposed when the Layer's
|
|
4
|
+
* scope closes.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { Context, Effect, Layer } from 'effect';
|
|
9
|
+
|
|
10
|
+
import { createResourceStore, type ResourceStore } from '../kernel/index.ts';
|
|
11
|
+
|
|
12
|
+
export class ResourceRegistry extends Context.Service<
|
|
13
|
+
ResourceRegistry,
|
|
14
|
+
{ readonly store: ResourceStore }
|
|
15
|
+
>()('effect-three/ResourceRegistry') {}
|
|
16
|
+
|
|
17
|
+
const acquireStore = Effect.sync(createResourceStore);
|
|
18
|
+
|
|
19
|
+
const toService = (store: ResourceStore) => ({ store });
|
|
20
|
+
|
|
21
|
+
const scopedStore = Effect.acquireRelease(acquireStore, (store) => Effect.sync(store.dispose));
|
|
22
|
+
|
|
23
|
+
/** Provides one shared resource store for the lifetime of the Layer. */
|
|
24
|
+
export const layer = Layer.effect(ResourceRegistry, scopedStore.pipe(Effect.map(toService)));
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A running scene: a kernel scene graph, a renderer and a frame subscription, all owned
|
|
3
|
+
* by the caller's `Scope`. Closing the scope stops the frame loop, then releases the
|
|
4
|
+
* event queue, the renderer and every resource the scene acquired, in that order.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { Clock, Effect, Exit, Match, Option, Queue, Result, Scope, Stream } from 'effect';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createSceneClock,
|
|
12
|
+
createSceneGraph,
|
|
13
|
+
type Behaviour,
|
|
14
|
+
createFramePacer,
|
|
15
|
+
createSceneLoop,
|
|
16
|
+
type Op,
|
|
17
|
+
type OverlayPlacement,
|
|
18
|
+
type LabelEngine,
|
|
19
|
+
type PhysicsEngine,
|
|
20
|
+
type ReconcileResult,
|
|
21
|
+
type RendererFactory,
|
|
22
|
+
type SceneDescription,
|
|
23
|
+
type SceneEvent,
|
|
24
|
+
type SceneClock,
|
|
25
|
+
type SceneGraph,
|
|
26
|
+
type SceneRenderer,
|
|
27
|
+
type Viewport,
|
|
28
|
+
} from '../kernel/index.ts';
|
|
29
|
+
import { InvalidScene, RendererUnavailable } from './errors.ts';
|
|
30
|
+
import { FrameSource } from './FrameSource.ts';
|
|
31
|
+
import { ResourceRegistry } from './ResourceRegistry.ts';
|
|
32
|
+
|
|
33
|
+
export type SceneRuntimeOptions = {
|
|
34
|
+
readonly scene: SceneDescription;
|
|
35
|
+
readonly renderer: RendererFactory;
|
|
36
|
+
readonly viewport: Viewport;
|
|
37
|
+
/** Events kept when the consumer falls behind; older events are dropped. Default 64. */
|
|
38
|
+
readonly eventCapacity?: number;
|
|
39
|
+
/** Behaviours nodes may name, beside the built-in ones. */
|
|
40
|
+
readonly behaviours?: Readonly<Record<string, Behaviour>>;
|
|
41
|
+
/** The physics engine for meshes with a `body`; without one, bodies stay put. */
|
|
42
|
+
readonly physics?: PhysicsEngine | undefined;
|
|
43
|
+
/** Draws `label` nodes (distance-field text); without one, labels are empty. */
|
|
44
|
+
readonly labels?: LabelEngine | undefined;
|
|
45
|
+
/** Receives where the overlays are after each drawn frame, to move their HTML there. */
|
|
46
|
+
readonly placeOverlays?: (placements: ReadonlyArray<OverlayPlacement>) => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const defaultEventCapacity = 64;
|
|
50
|
+
const noBehaviours: Readonly<Record<string, Behaviour>> = {};
|
|
51
|
+
const ignorePlacements = (_placements: ReadonlyArray<OverlayPlacement>): void => undefined;
|
|
52
|
+
const nanosPerMillisecond = 1_000_000;
|
|
53
|
+
const nanosPerSecond = 1_000_000_000;
|
|
54
|
+
|
|
55
|
+
const toResult = (result: ReconcileResult): Result.Result<ReadonlyArray<Op>, InvalidScene> =>
|
|
56
|
+
Match.valueTags(result, {
|
|
57
|
+
Applied: (applied) => Result.succeed(applied.ops),
|
|
58
|
+
Invalid: ({ error }) => Result.fail(new InvalidScene({ error })),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const applyViewport = (graph: SceneGraph, renderer: SceneRenderer, viewport: Viewport): void => {
|
|
62
|
+
graph.resize(viewport.width, viewport.height);
|
|
63
|
+
renderer.setViewport(viewport);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** The engines a scene graph hands work to, and the renderer whose kit shades it. */
|
|
67
|
+
type Engines = Pick<SceneRuntimeOptions, 'physics' | 'labels' | 'renderer'>;
|
|
68
|
+
|
|
69
|
+
const sceneGraph =
|
|
70
|
+
(
|
|
71
|
+
registry: ResourceRegistry['Service'],
|
|
72
|
+
behaviours: Readonly<Record<string, Behaviour>>,
|
|
73
|
+
engines: Engines,
|
|
74
|
+
) =>
|
|
75
|
+
() =>
|
|
76
|
+
createSceneGraph({
|
|
77
|
+
store: registry.store,
|
|
78
|
+
behaviours,
|
|
79
|
+
physics: engines.physics,
|
|
80
|
+
labels: engines.labels,
|
|
81
|
+
kit: engines.renderer.kit,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reconciles at the current scene time. An idle scene draws no frames, so its last tick
|
|
86
|
+
* can be long past; catching up first makes new transitions start now, not back then.
|
|
87
|
+
*/
|
|
88
|
+
const reconcileNow = (graph: SceneGraph, sceneClock: SceneClock, scene: SceneDescription) => {
|
|
89
|
+
graph.tick(sceneClock.elapsedSeconds());
|
|
90
|
+
return graph.reconcile(scene);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const viewportChange = (graph: SceneGraph, renderer: SceneRenderer, viewport: Viewport) => () =>
|
|
94
|
+
applyViewport(graph, renderer, viewport);
|
|
95
|
+
|
|
96
|
+
const animationChange = (sceneClock: SceneClock, graph: SceneGraph, animating: boolean) => () => {
|
|
97
|
+
sceneClock.setAnimating(animating);
|
|
98
|
+
graph.setAnimating(animating);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const acquireGraph = (
|
|
102
|
+
registry: ResourceRegistry['Service'],
|
|
103
|
+
behaviours: Readonly<Record<string, Behaviour>>,
|
|
104
|
+
engines: Engines,
|
|
105
|
+
) => {
|
|
106
|
+
const create = Effect.sync(sceneGraph(registry, behaviours, engines));
|
|
107
|
+
return Effect.acquireRelease(create, (graph) => Effect.sync(graph.dispose));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const acquireRenderer = (factory: RendererFactory) => {
|
|
111
|
+
const create = Effect.try({
|
|
112
|
+
try: factory.create,
|
|
113
|
+
catch: (cause) => new RendererUnavailable({ cause }),
|
|
114
|
+
});
|
|
115
|
+
return Effect.acquireRelease(create, (renderer) => Effect.sync(renderer.dispose));
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Waits for a renderer that starts asynchronously (WebGPU); a failed start is unavailable. */
|
|
119
|
+
const awaitReady = (renderer: SceneRenderer) =>
|
|
120
|
+
Option.match(Option.fromNullishOr(renderer.ready), {
|
|
121
|
+
onNone: () => Effect.void,
|
|
122
|
+
onSome: (ready) =>
|
|
123
|
+
Effect.tryPromise({ try: ready, catch: (cause) => new RendererUnavailable({ cause }) }),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const invalidationListening = (graph: SceneGraph, onInvalidated: () => void) => () =>
|
|
127
|
+
graph.onInvalidated(onInvalidated);
|
|
128
|
+
|
|
129
|
+
const acquireInvalidation = (graph: SceneGraph, onInvalidated: () => void) => {
|
|
130
|
+
const listen = Effect.sync(invalidationListening(graph, onInvalidated));
|
|
131
|
+
return Effect.acquireRelease(listen, Effect.sync);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const acquireEvents = (capacity: number) => {
|
|
135
|
+
const create = Queue.sliding<SceneEvent>(capacity);
|
|
136
|
+
return Effect.acquireRelease(create, Queue.shutdown);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function* startScene(options: SceneRuntimeOptions) {
|
|
140
|
+
const registry = yield* ResourceRegistry;
|
|
141
|
+
const frames = yield* FrameSource;
|
|
142
|
+
const clock = yield* Clock.Clock;
|
|
143
|
+
|
|
144
|
+
const graph = yield* acquireGraph(registry, options.behaviours ?? noBehaviours, options);
|
|
145
|
+
const startupOps = yield* Effect.fromResult(toResult(graph.reconcile(options.scene)));
|
|
146
|
+
const renderer = yield* acquireRenderer(options.renderer);
|
|
147
|
+
yield* awaitReady(renderer);
|
|
148
|
+
applyViewport(graph, renderer, options.viewport);
|
|
149
|
+
const events = yield* acquireEvents(options.eventCapacity ?? defaultEventCapacity);
|
|
150
|
+
|
|
151
|
+
const sceneClock = createSceneClock(
|
|
152
|
+
() => Number(clock.monotonicTimeNanosUnsafe()) / nanosPerSecond,
|
|
153
|
+
);
|
|
154
|
+
const loop = createSceneLoop({
|
|
155
|
+
graph,
|
|
156
|
+
renderer,
|
|
157
|
+
elapsedSeconds: sceneClock.elapsedSeconds,
|
|
158
|
+
emit: (event) => {
|
|
159
|
+
Queue.offerUnsafe(events, event);
|
|
160
|
+
},
|
|
161
|
+
now: () => Number(clock.monotonicTimeNanosUnsafe()) / nanosPerMillisecond,
|
|
162
|
+
placeOverlays: options.placeOverlays ?? ignorePlacements,
|
|
163
|
+
});
|
|
164
|
+
yield* Effect.addFinalizer(() => Effect.sync(loop.dispose));
|
|
165
|
+
const pacer = createFramePacer(loop);
|
|
166
|
+
pacer.attach(yield* frames.subscribe(pacer.frame));
|
|
167
|
+
yield* acquireInvalidation(graph, pacer.wake);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
/** The hosted scene graph, for inspection. */
|
|
171
|
+
graph,
|
|
172
|
+
/** The operations that built the initial scene. */
|
|
173
|
+
startupOps,
|
|
174
|
+
/**
|
|
175
|
+
* Applies a new description and returns the operations it took. Structurally equal
|
|
176
|
+
* descriptions return no operations and cost no host changes.
|
|
177
|
+
*/
|
|
178
|
+
update: (scene: SceneDescription) =>
|
|
179
|
+
Effect.suspend(() => Effect.fromResult(toResult(reconcileNow(graph, sceneClock, scene)))),
|
|
180
|
+
/** Resizes the camera aspect and the renderer's drawing surface. */
|
|
181
|
+
resize: (viewport: Viewport) => Effect.sync(viewportChange(graph, renderer, viewport)),
|
|
182
|
+
/** Freezes (`false`) or resumes (`true`) behaviours; frozen, transitions land at once. */
|
|
183
|
+
setAnimating: (animating: boolean) =>
|
|
184
|
+
Effect.sync(animationChange(sceneClock, graph, animating)),
|
|
185
|
+
/**
|
|
186
|
+
* Draws one frame now, whatever the frame mode: React Three Fiber's `advance`, for
|
|
187
|
+
* `frames: 'never'` scenes and frame-exact capture.
|
|
188
|
+
*/
|
|
189
|
+
advance: Effect.sync(pacer.advance),
|
|
190
|
+
/**
|
|
191
|
+
* Draws one frame now, synchronously, for a host's own frame source to call directly:
|
|
192
|
+
* an immersive (WebXR) session's animation loop.
|
|
193
|
+
*/
|
|
194
|
+
drawNow: pacer.advance,
|
|
195
|
+
/** Routes pointer input. Synchronous, so DOM listeners can call it directly. */
|
|
196
|
+
pointer: loop.pointer,
|
|
197
|
+
/** Routes XR input: aims, selects, sticks, buttons and surfaces. Synchronous, as `pointer`. */
|
|
198
|
+
xrInput: loop.xrInput,
|
|
199
|
+
/** Scene events, newest kept when the consumer falls behind. */
|
|
200
|
+
events: Stream.fromQueue(events),
|
|
201
|
+
/** Removes and returns every pending event without waiting. */
|
|
202
|
+
drainEvents: Queue.clear(events),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Startup is atomic: everything is acquired into a child of the caller's scope, which is
|
|
207
|
+
// closed immediately if startup fails or is interrupted. Otherwise a partial scene would
|
|
208
|
+
// stay allocated until the caller's scope closed (under Foldkit: until unmount).
|
|
209
|
+
function* startAtomically(options: SceneRuntimeOptions) {
|
|
210
|
+
const parent = yield* Effect.scope;
|
|
211
|
+
const child = yield* Scope.fork(parent);
|
|
212
|
+
return yield* Effect.gen(() => startScene(options)).pipe(
|
|
213
|
+
Scope.provide(child),
|
|
214
|
+
Effect.onError((cause) => Scope.close(child, Exit.failCause(cause))),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Starts a scene in the current scope. Fails with `InvalidScene` if the initial
|
|
220
|
+
* description is rejected and `RendererUnavailable` if the renderer can't be created.
|
|
221
|
+
* A failed or interrupted start releases everything it acquired before returning.
|
|
222
|
+
*/
|
|
223
|
+
export const make = (options: SceneRuntimeOptions) => {
|
|
224
|
+
const program = Effect.gen(() => startAtomically(options));
|
|
225
|
+
return program.pipe(Effect.withSpan('effect-three/SceneRuntime.make'));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** The handle `make` returns. */
|
|
229
|
+
export type SceneHandle = Effect.Success<ReturnType<typeof make>>;
|