@opsydyn/effect-three 0.2.0 → 0.4.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 +21 -1
- package/README.md +14 -1
- package/dist/{index-01kwfekm.js → index-h3vf50fn.js} +2418 -85
- package/dist/index-h3vf50fn.js.map +53 -0
- package/dist/index-ttwy0znv.js +145 -0
- package/dist/index-ttwy0znv.js.map +12 -0
- package/dist/index.js +543 -149
- package/dist/index.js.map +8 -6
- package/dist/kernel/behaviours.d.ts.map +1 -1
- package/dist/kernel/boxSelect.d.ts +22 -0
- package/dist/kernel/boxSelect.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +197 -6
- package/dist/kernel/build.d.ts.map +1 -1
- package/dist/kernel/cameraMotion.d.ts +3 -1
- package/dist/kernel/cameraMotion.d.ts.map +1 -1
- package/dist/kernel/cameraShake.d.ts +33 -0
- package/dist/kernel/cameraShake.d.ts.map +1 -0
- package/dist/kernel/describeScene.d.ts +50 -0
- package/dist/kernel/describeScene.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +240 -2
- package/dist/kernel/description.d.ts.map +1 -1
- package/dist/kernel/diff.d.ts +18 -1
- package/dist/kernel/diff.d.ts.map +1 -1
- package/dist/kernel/equality.d.ts +13 -1
- package/dist/kernel/equality.d.ts.map +1 -1
- package/dist/kernel/gizmo.d.ts +69 -0
- package/dist/kernel/gizmo.d.ts.map +1 -0
- package/dist/kernel/history.d.ts +61 -0
- package/dist/kernel/history.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +13 -2
- package/dist/kernel/index.d.ts.map +1 -1
- package/dist/kernel/index.js +116 -3
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/inspect.d.ts +85 -0
- package/dist/kernel/inspect.d.ts.map +1 -0
- package/dist/kernel/lineGeometry.d.ts +25 -0
- package/dist/kernel/lineGeometry.d.ts.map +1 -0
- package/dist/kernel/lockstep.d.ts +114 -0
- package/dist/kernel/lockstep.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts.map +1 -1
- package/dist/kernel/materials.d.ts.map +1 -1
- package/dist/kernel/pivot.d.ts +54 -0
- package/dist/kernel/pivot.d.ts.map +1 -0
- package/dist/kernel/pointBuffers.d.ts +17 -0
- package/dist/kernel/pointBuffers.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +12 -0
- package/dist/kernel/pointer.d.ts.map +1 -1
- package/dist/kernel/renderKit.d.ts +21 -1
- package/dist/kernel/renderKit.d.ts.map +1 -1
- package/dist/kernel/renderer.d.ts +6 -0
- package/dist/kernel/renderer.d.ts.map +1 -1
- package/dist/kernel/resources.d.ts.map +1 -1
- package/dist/kernel/sceneChanges.d.ts +87 -0
- package/dist/kernel/sceneChanges.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +6 -1
- package/dist/kernel/sceneGraph.d.ts.map +1 -1
- package/dist/kernel/seeded.d.ts +9 -0
- package/dist/kernel/seeded.d.ts.map +1 -0
- package/dist/kernel/shots.d.ts +26 -0
- package/dist/kernel/shots.d.ts.map +1 -0
- package/dist/kernel/tape.d.ts +38 -0
- package/dist/kernel/tape.d.ts.map +1 -0
- package/dist/kernel/trail.d.ts +31 -0
- package/dist/kernel/trail.d.ts.map +1 -0
- package/dist/kernel/viewportGizmo.d.ts +25 -0
- package/dist/kernel/viewportGizmo.d.ts.map +1 -0
- package/dist/runtime/Inspector.d.ts +20 -0
- package/dist/runtime/Inspector.d.ts.map +1 -0
- package/dist/runtime/SceneFile.d.ts +448 -0
- package/dist/runtime/SceneFile.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +614 -5
- package/dist/runtime/SceneSchema.d.ts.map +1 -1
- package/dist/runtime/errors.d.ts +9 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/webgl/index.d.ts.map +1 -1
- package/dist/webgl/index.js +228 -3
- package/dist/webgl/index.js.map +6 -4
- package/dist/webgl/line.d.ts +4 -0
- package/dist/webgl/line.d.ts.map +1 -0
- package/dist/webgl/points.d.ts +4 -0
- package/dist/webgl/points.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts.map +1 -1
- package/dist/webgpu/index.js +206 -26
- package/dist/webgpu/index.js.map +6 -4
- package/dist/webgpu/line.d.ts +4 -0
- package/dist/webgpu/line.d.ts.map +1 -0
- package/dist/webgpu/points.d.ts +4 -0
- package/dist/webgpu/points.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/kernel/behaviours.ts +5 -0
- package/src/kernel/bodySources.ts +1 -1
- package/src/kernel/boxSelect.ts +73 -0
- package/src/kernel/build.ts +430 -0
- package/src/kernel/cameraMotion.ts +3 -0
- package/src/kernel/cameraShake.ts +87 -0
- package/src/kernel/describeScene.ts +224 -0
- package/src/kernel/description.ts +266 -1
- package/src/kernel/diff.ts +46 -1
- package/src/kernel/equality.ts +148 -0
- package/src/kernel/gizmo.ts +478 -0
- package/src/kernel/history.ts +118 -0
- package/src/kernel/index.ts +102 -0
- package/src/kernel/inspect.ts +214 -0
- package/src/kernel/lineGeometry.ts +75 -0
- package/src/kernel/lockstep.ts +237 -0
- package/src/kernel/loop.ts +5 -0
- package/src/kernel/materials.ts +3 -0
- package/src/kernel/pivot.ts +418 -0
- package/src/kernel/pointBuffers.ts +115 -0
- package/src/kernel/pointer.ts +63 -12
- package/src/kernel/renderKit.ts +23 -1
- package/src/kernel/renderer.ts +6 -0
- package/src/kernel/resources.ts +69 -0
- package/src/kernel/sceneChanges.ts +265 -0
- package/src/kernel/sceneGraph.ts +461 -9
- package/src/kernel/seeded.ts +18 -0
- package/src/kernel/shots.ts +167 -0
- package/src/kernel/tape.ts +124 -0
- package/src/kernel/trail.ts +170 -0
- package/src/kernel/viewportGizmo.ts +194 -0
- package/src/runtime/Inspector.ts +43 -0
- package/src/runtime/SceneFile.ts +46 -0
- package/src/runtime/SceneSchema.ts +305 -4
- package/src/runtime/errors.ts +6 -0
- package/src/runtime/index.ts +3 -1
- package/src/webgl/index.ts +4 -0
- package/src/webgl/line.ts +66 -0
- package/src/webgl/points.ts +205 -0
- package/src/webgpu/index.ts +4 -0
- package/src/webgpu/line.ts +67 -0
- package/src/webgpu/points.ts +193 -0
- package/dist/index-01kwfekm.js.map +0 -40
package/src/kernel/sceneGraph.ts
CHANGED
|
@@ -21,20 +21,25 @@ import {
|
|
|
21
21
|
OrthographicCamera,
|
|
22
22
|
PerspectiveCamera,
|
|
23
23
|
PointLight,
|
|
24
|
+
Quaternion,
|
|
24
25
|
Scene,
|
|
25
26
|
SpotLight,
|
|
26
27
|
Vector3,
|
|
27
28
|
} from 'three';
|
|
29
|
+
import { clone as cloneSkinned } from 'three/addons/utils/SkeletonUtils.js';
|
|
28
30
|
|
|
29
31
|
import { clipRunning, clipTime } from './animation.ts';
|
|
30
32
|
import { writeRotation } from './behaviours.ts';
|
|
31
33
|
import { type BodyCollision, type BodyContactForce, createBodyHost } from './bodies.ts';
|
|
32
34
|
import { type BodySource, instanceSources, nodeSource, placeObject } from './bodySources.ts';
|
|
33
|
-
import { renderSettings, scene as describeScene } from './build.ts';
|
|
35
|
+
import { orthographicCamera, renderSettings, scene as describeScene } from './build.ts';
|
|
34
36
|
import { type CameraMotion, createCameraMotion } from './cameraMotion.ts';
|
|
37
|
+
import { type CameraShaker, createCameraShaker } from './cameraShake.ts';
|
|
35
38
|
import type {
|
|
39
|
+
Hud,
|
|
36
40
|
AmbientLightNode,
|
|
37
41
|
BorrowedNode,
|
|
42
|
+
Edges,
|
|
38
43
|
DirectionalLightNode,
|
|
39
44
|
GridNode,
|
|
40
45
|
HemisphereLightNode,
|
|
@@ -42,6 +47,8 @@ import type {
|
|
|
42
47
|
Instance,
|
|
43
48
|
InstancesNode,
|
|
44
49
|
LabelNode,
|
|
50
|
+
LineNode,
|
|
51
|
+
PointsNode,
|
|
45
52
|
MaterialRef,
|
|
46
53
|
ShaderGridNode,
|
|
47
54
|
MeshNode,
|
|
@@ -65,9 +72,16 @@ import type {
|
|
|
65
72
|
ViewRect,
|
|
66
73
|
} from './description.ts';
|
|
67
74
|
import { diff, type Field, type Op } from './diff.ts';
|
|
68
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
contactShadowsEquals,
|
|
77
|
+
edgesEquals,
|
|
78
|
+
environmentEquals,
|
|
79
|
+
overlaysEqual,
|
|
80
|
+
skyEquals,
|
|
81
|
+
} from './equality.ts';
|
|
69
82
|
import { createInstances, fits, type HostInstances, writeInstances } from './instances.ts';
|
|
70
83
|
import type { HostedLabel, LabelEngine, LabelLayout } from './labels.ts';
|
|
84
|
+
import { edgeLine, edgePoints } from './lineGeometry.ts';
|
|
71
85
|
import type { HostMaterial } from './materials.ts';
|
|
72
86
|
import {
|
|
73
87
|
type Behaviour,
|
|
@@ -77,7 +91,14 @@ import {
|
|
|
77
91
|
resetPose,
|
|
78
92
|
} from './motion.ts';
|
|
79
93
|
import type { BodyPose, PhysicsEngine } from './physics.ts';
|
|
80
|
-
import
|
|
94
|
+
import { animatedLook } from './pointBuffers.ts';
|
|
95
|
+
import type {
|
|
96
|
+
HostedLine,
|
|
97
|
+
HostedPoints,
|
|
98
|
+
HostedShaderGrid,
|
|
99
|
+
HostedSky,
|
|
100
|
+
RenderKit,
|
|
101
|
+
} from './renderKit.ts';
|
|
81
102
|
import { createResourceStore, type ResourceStore } from './resources.ts';
|
|
82
103
|
import {
|
|
83
104
|
type DescriptionError,
|
|
@@ -86,6 +107,7 @@ import {
|
|
|
86
107
|
type ParentKey,
|
|
87
108
|
type SceneIndex,
|
|
88
109
|
} from './sceneIndex.ts';
|
|
110
|
+
import { createTrailRibbon, type TrailRibbon } from './trail.ts';
|
|
89
111
|
import { applyTween, startTween, type Tween } from './tween.ts';
|
|
90
112
|
|
|
91
113
|
/** The mesh type the kernel hosts. */
|
|
@@ -139,6 +161,20 @@ type Entry =
|
|
|
139
161
|
readonly _tag: 'HemisphereLight';
|
|
140
162
|
node: HemisphereLightNode;
|
|
141
163
|
readonly object: HemisphereLight;
|
|
164
|
+
}
|
|
165
|
+
// `hosted` is the points the render kit drew, inside the wrapper; none without a kit.
|
|
166
|
+
| {
|
|
167
|
+
readonly _tag: 'Points';
|
|
168
|
+
node: PointsNode;
|
|
169
|
+
readonly object: Group;
|
|
170
|
+
readonly hosted: HostedPoints | undefined;
|
|
171
|
+
}
|
|
172
|
+
// `hosted` is the line the render kit drew, inside the wrapper; none without a kit.
|
|
173
|
+
| {
|
|
174
|
+
readonly _tag: 'Line';
|
|
175
|
+
node: LineNode;
|
|
176
|
+
readonly object: Group;
|
|
177
|
+
readonly hosted: HostedLine | undefined;
|
|
142
178
|
};
|
|
143
179
|
|
|
144
180
|
/** Entries with a transform: position, rotation, scale and transitions. */
|
|
@@ -151,12 +187,26 @@ type Player = {
|
|
|
151
187
|
readonly startedAt: number;
|
|
152
188
|
};
|
|
153
189
|
|
|
190
|
+
/** A mesh's edge lines: the kit's line, and the geometry and look it was built from. */
|
|
191
|
+
type EdgeLines = {
|
|
192
|
+
readonly hosted: HostedLine;
|
|
193
|
+
geometry: BufferGeometry | undefined;
|
|
194
|
+
edges: Edges | undefined;
|
|
195
|
+
};
|
|
196
|
+
|
|
154
197
|
/** Descriptions with a transform. */
|
|
155
|
-
type PlacedNode =
|
|
198
|
+
type PlacedNode =
|
|
199
|
+
| GroupNode
|
|
200
|
+
| MeshNode
|
|
201
|
+
| InstancesNode
|
|
202
|
+
| BorrowedNode
|
|
203
|
+
| LabelNode
|
|
204
|
+
| LineNode
|
|
205
|
+
| PointsNode;
|
|
156
206
|
|
|
157
207
|
type Placed = Extract<
|
|
158
208
|
Entry,
|
|
159
|
-
{ readonly _tag: 'Group' | 'Mesh' | 'Instances' | 'Borrowed' | 'Label' }
|
|
209
|
+
{ readonly _tag: 'Group' | 'Mesh' | 'Instances' | 'Borrowed' | 'Label' | 'Line' | 'Points' }
|
|
160
210
|
>;
|
|
161
211
|
|
|
162
212
|
/** Entries drawing a geometry with a material. */
|
|
@@ -167,12 +217,17 @@ const isPlaced = (entry: Entry): entry is Placed =>
|
|
|
167
217
|
entry._tag === 'Mesh' ||
|
|
168
218
|
entry._tag === 'Instances' ||
|
|
169
219
|
entry._tag === 'Borrowed' ||
|
|
170
|
-
entry._tag === 'Label'
|
|
220
|
+
entry._tag === 'Label' ||
|
|
221
|
+
entry._tag === 'Line' ||
|
|
222
|
+
entry._tag === 'Points';
|
|
171
223
|
|
|
172
224
|
const isSurfaced = (entry: Entry): entry is Surfaced =>
|
|
173
225
|
entry._tag === 'Mesh' || entry._tag === 'Instances';
|
|
174
226
|
|
|
175
227
|
/** A render texture as hosted: its id, size and the nested graph that draws it. */
|
|
228
|
+
/** The HUD as hosted: its nested graph, drawn over the scene by the frame loop. */
|
|
229
|
+
export type HostedHud = { readonly graph: SceneGraph };
|
|
230
|
+
|
|
176
231
|
export type HostedTexture = {
|
|
177
232
|
readonly id: string;
|
|
178
233
|
readonly graph: SceneGraph;
|
|
@@ -229,6 +284,8 @@ export type SceneGraph = {
|
|
|
229
284
|
readonly turned: (view: string | undefined) => Vec3 | undefined;
|
|
230
285
|
/** Render textures, each a nested graph to draw into its texture (by the frame loop). */
|
|
231
286
|
readonly renderTextures: () => ReadonlyArray<HostedTexture>;
|
|
287
|
+
/** The HUD, a nested graph drawn over the scene (by the frame loop), if there is one. */
|
|
288
|
+
readonly hud: () => HostedHud | undefined;
|
|
232
289
|
/** Whether the scene claims the pointer's gestures from the page. */
|
|
233
290
|
readonly gestures: () => Gestures;
|
|
234
291
|
/** Points to show HTML at; the frame loop places them after drawing. */
|
|
@@ -426,6 +483,14 @@ const noInstances: ReadonlyArray<Instance> = [];
|
|
|
426
483
|
const emptyScene = describeScene({});
|
|
427
484
|
|
|
428
485
|
/** Creates an empty scene graph. */
|
|
486
|
+
/**
|
|
487
|
+
* A copy of a lent object for a second place to show it, sharing its geometry and
|
|
488
|
+
* materials, as Drei's `Clone` (`bf6f4ad`, `src/core/Clone.tsx`): a skinned model through
|
|
489
|
+
* three's `SkeletonUtils.clone`, so that the copy has bones of its own to animate.
|
|
490
|
+
*/
|
|
491
|
+
const cloneLoan = (lent: Object3D): Object3D =>
|
|
492
|
+
lent.getObjectByProperty('isSkinnedMesh', true) === undefined ? lent.clone() : cloneSkinned(lent);
|
|
493
|
+
|
|
429
494
|
export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph => {
|
|
430
495
|
const store = options.store ?? createResourceStore();
|
|
431
496
|
const ownsStore = options.store === undefined;
|
|
@@ -437,6 +502,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
437
502
|
{ readonly graph: SceneGraph; description: RenderTexture }
|
|
438
503
|
>();
|
|
439
504
|
let textureList: ReadonlyArray<HostedTexture> = [];
|
|
505
|
+
let hudHosted:
|
|
506
|
+
| { readonly graph: SceneGraph; description: Hud; parent: SceneDescription }
|
|
507
|
+
| undefined;
|
|
440
508
|
let viewDescriptions: ReadonlyArray<View> = [];
|
|
441
509
|
let gestures: Gestures = emptyScene.gestures;
|
|
442
510
|
let overlayList: ReadonlyArray<Overlay> = emptyScene.overlays;
|
|
@@ -470,8 +538,22 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
470
538
|
hostedSky = undefined;
|
|
471
539
|
}
|
|
472
540
|
const cameraMotion = createCameraMotion();
|
|
541
|
+
const cameraShaker = createCameraShaker();
|
|
473
542
|
/** Each view's camera motion, by the view's key. */
|
|
474
543
|
const viewMotions = new Map<string, CameraMotion>();
|
|
544
|
+
/** Each view's camera shake, by the view's key. */
|
|
545
|
+
const viewShakers = new Map<string, CameraShaker>();
|
|
546
|
+
const shaking = (): boolean => {
|
|
547
|
+
if (cameraShaker.shaking(elapsed)) return true;
|
|
548
|
+
for (const shaker of viewShakers.values()) if (shaker.shaking(elapsed)) return true;
|
|
549
|
+
return false;
|
|
550
|
+
};
|
|
551
|
+
/** Shakes a view's camera on top of where its motion aims it. */
|
|
552
|
+
const shakeView = (view: HostedView): void => {
|
|
553
|
+
const motion = viewMotions.get(view.key);
|
|
554
|
+
if (motion === undefined) return;
|
|
555
|
+
viewShakers.get(view.key)?.apply(view.camera, motion.target(), elapsed, animating);
|
|
556
|
+
};
|
|
475
557
|
const viewsMoving = (): boolean => {
|
|
476
558
|
for (const motion of viewMotions.values()) if (motion.moving()) return true;
|
|
477
559
|
return false;
|
|
@@ -505,6 +587,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
505
587
|
applyLens(hostedCamera, description.camera);
|
|
506
588
|
const motion = viewMotions.get(description.key) ?? createCameraMotion();
|
|
507
589
|
viewMotions.set(description.key, motion);
|
|
590
|
+
const shaker = viewShakers.get(description.key) ?? createCameraShaker();
|
|
591
|
+
viewShakers.set(description.key, shaker);
|
|
592
|
+
shaker.describe(description.camera.shake, elapsed);
|
|
508
593
|
// As the scene's camera: a new kind of camera has nothing on screen to travel from.
|
|
509
594
|
motion.aim(hostedCamera, description.camera, elapsed, hostedCamera !== previous || !animating);
|
|
510
595
|
const area = areaOf(description.rect);
|
|
@@ -520,10 +605,41 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
520
605
|
for (const key of viewMotions.keys()) {
|
|
521
606
|
if (!described.some((description) => description.key === key)) viewMotions.delete(key);
|
|
522
607
|
}
|
|
608
|
+
for (const key of viewShakers.keys()) {
|
|
609
|
+
if (!described.some((description) => description.key === key)) viewShakers.delete(key);
|
|
610
|
+
}
|
|
523
611
|
};
|
|
524
612
|
const entries = new Map<string, Entry>();
|
|
525
613
|
const keys = new WeakMap<Object3D, string>();
|
|
526
614
|
const spinning = new Set<Entry>();
|
|
615
|
+
// Billboards: turned towards the camera each frame, after everything else has moved.
|
|
616
|
+
const facing = new Set<Entry>();
|
|
617
|
+
const cameraTurn = new Quaternion();
|
|
618
|
+
const parentTurn = new Quaternion();
|
|
619
|
+
// Points that move by themselves (sparkles, stars), ticked in scene time while animating.
|
|
620
|
+
const sparkling = new Set<Entry>();
|
|
621
|
+
const trackSparkle = (entry: Entry, node: PointsNode): void => {
|
|
622
|
+
if (animatedLook(node.look)) sparkling.add(entry);
|
|
623
|
+
else sparkling.delete(entry);
|
|
624
|
+
if (entry._tag === 'Points') entry.hosted?.tick(animating ? elapsed : 0);
|
|
625
|
+
};
|
|
626
|
+
const sparkle = (entry: Entry): void => {
|
|
627
|
+
if (entry._tag === 'Points') entry.hosted?.tick(elapsed);
|
|
628
|
+
};
|
|
629
|
+
// Groups and meshes with trails: each ribbon, in the scene's space.
|
|
630
|
+
const trailing = new Map<Entry, TrailRibbon>();
|
|
631
|
+
const trailPlace = new Vector3();
|
|
632
|
+
const eye = new Vector3();
|
|
633
|
+
const trailsMoving = (): boolean => {
|
|
634
|
+
for (const ribbon of trailing.values()) if (!ribbon.settled()) return true;
|
|
635
|
+
return false;
|
|
636
|
+
};
|
|
637
|
+
// Meshes with edge lines: rebuilt when the mesh's geometry or the edges' look changes.
|
|
638
|
+
const edged = new Map<Entry, EdgeLines>();
|
|
639
|
+
// Groups with levels of detail: one child shown at a time, by the camera's distance.
|
|
640
|
+
const detailing = new Set<Entry>();
|
|
641
|
+
const cameraPlace = new Vector3();
|
|
642
|
+
const groupPlace = new Vector3();
|
|
527
643
|
const playing = new Set<Borrowing>();
|
|
528
644
|
const registry: Readonly<Record<string, Behaviour>> = {
|
|
529
645
|
...builtinBehaviours,
|
|
@@ -580,6 +696,152 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
580
696
|
writeRotation(entry.object.rotation, node.rotation, elapsed);
|
|
581
697
|
if (node.rotation._tag === 'Spin') spinning.add(entry);
|
|
582
698
|
else spinning.delete(entry);
|
|
699
|
+
if (node.rotation._tag === 'Billboard') facing.add(entry);
|
|
700
|
+
else facing.delete(entry);
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Drei's `Billboard`: the node's rotation is its parent's world rotation undone, then the
|
|
705
|
+
* camera's, so its world rotation is the camera's; locked axes go back to 0.
|
|
706
|
+
*/
|
|
707
|
+
const faceCamera = (entry: Entry): void => {
|
|
708
|
+
if (!isPlaced(entry) || entry.node.rotation._tag !== 'Billboard') return;
|
|
709
|
+
const { lockX, lockY, lockZ } = entry.node.rotation;
|
|
710
|
+
const object = entry.object;
|
|
711
|
+
parentTurn.identity();
|
|
712
|
+
object.parent?.getWorldQuaternion(parentTurn);
|
|
713
|
+
object.quaternion.copy(cameraTurn).premultiply(parentTurn.invert());
|
|
714
|
+
if (lockX) object.rotation.x = 0;
|
|
715
|
+
if (lockY) object.rotation.y = 0;
|
|
716
|
+
if (lockZ) object.rotation.z = 0;
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
/** Where a trail follows its node: the world position, or with `local` its own. */
|
|
720
|
+
const trailTarget = (object: Object3D, local: boolean): Vector3 =>
|
|
721
|
+
local ? trailPlace.copy(object.position) : object.getWorldPosition(trailPlace);
|
|
722
|
+
|
|
723
|
+
/** Adds, restyles or removes a node's trail as its description says. */
|
|
724
|
+
const trackTrail = (entry: Entry, node: GroupNode | MeshNode): void => {
|
|
725
|
+
const current = trailing.get(entry);
|
|
726
|
+
if (node.trail === null) {
|
|
727
|
+
if (current === undefined) return;
|
|
728
|
+
current.object.removeFromParent();
|
|
729
|
+
current.dispose();
|
|
730
|
+
trailing.delete(entry);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const start = trailTarget(entry.object, node.trail.local);
|
|
734
|
+
if (current !== undefined) {
|
|
735
|
+
current.restyle(node.trail, start, elapsed);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const ribbon = createTrailRibbon(node.trail, start, elapsed);
|
|
739
|
+
scene.add(ribbon.object);
|
|
740
|
+
trailing.set(entry, ribbon);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
/** Records where a trailing node is now, and turns its ribbon to the camera. */
|
|
744
|
+
const followTrail = (ribbon: TrailRibbon, entry: Entry): void => {
|
|
745
|
+
if (entry._tag !== 'Group' && entry._tag !== 'Mesh') return;
|
|
746
|
+
if (entry.node.trail === null) return;
|
|
747
|
+
ribbon.record(trailTarget(entry.object, entry.node.trail.local), elapsed);
|
|
748
|
+
ribbon.face(eye);
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
/** Drei's `Edges` turns off picking on its line: hits belong to the mesh. */
|
|
752
|
+
const unpickable = (part: Object3D): void => {
|
|
753
|
+
part.raycast = () => undefined;
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
/** Adds, keeps or removes a mesh's edge lines as its description says. */
|
|
757
|
+
const trackEdges = (entry: Entry, node: MeshNode): void => {
|
|
758
|
+
const current = edged.get(entry);
|
|
759
|
+
const draw = options.kit?.line;
|
|
760
|
+
if (node.edges === null || draw === undefined) {
|
|
761
|
+
if (current === undefined) return;
|
|
762
|
+
current.hosted.object.removeFromParent();
|
|
763
|
+
current.hosted.dispose();
|
|
764
|
+
edged.delete(entry);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (current !== undefined) return;
|
|
768
|
+
const hosted = draw(edgeLine(node.key, node.edges, []));
|
|
769
|
+
entry.object.add(hosted.object);
|
|
770
|
+
edged.set(entry, { hosted, geometry: undefined, edges: undefined });
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Builds the edge lines again when the mesh shows another geometry (its own changed, a
|
|
775
|
+
* shared one was replaced, a lent one arrived) or the edges' look changed.
|
|
776
|
+
*/
|
|
777
|
+
const refreshEdges = (lines: EdgeLines, entry: Entry): void => {
|
|
778
|
+
if (entry._tag !== 'Mesh' || entry.node.edges === null) return;
|
|
779
|
+
const { geometry } = entry.object;
|
|
780
|
+
const look = entry.node.edges;
|
|
781
|
+
if (
|
|
782
|
+
geometry === lines.geometry &&
|
|
783
|
+
lines.edges !== undefined &&
|
|
784
|
+
edgesEquals(look, lines.edges)
|
|
785
|
+
) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
lines.hosted.update(edgeLine(entry.node.key, look, edgePoints(geometry, look.threshold)));
|
|
789
|
+
lines.hosted.object.traverse(unpickable);
|
|
790
|
+
lines.geometry = geometry;
|
|
791
|
+
lines.edges = look;
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
/** The object of a group's `index`th child, as described. */
|
|
795
|
+
const childObject = (node: GroupNode, index: number): Object3D | undefined => {
|
|
796
|
+
const child = node.children[index];
|
|
797
|
+
return child === undefined ? undefined : entries.get(child.key)?.object;
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* three's `LOD.update`, as Drei's `Detailed` runs it each frame, over the group's children
|
|
802
|
+
* in order: the first level shows, each later one takes over once the camera is at least
|
|
803
|
+
* its distance away (less its hysteresis while it shows), and the rest hide.
|
|
804
|
+
*/
|
|
805
|
+
const showLevel = (entry: Entry): void => {
|
|
806
|
+
if (entry._tag !== 'Group') return;
|
|
807
|
+
const { node } = entry;
|
|
808
|
+
const { detail } = node;
|
|
809
|
+
if (detail === null) return;
|
|
810
|
+
const { distances, hysteresis } = detail;
|
|
811
|
+
const count = node.children.length;
|
|
812
|
+
if (count < 2) return;
|
|
813
|
+
entry.object.getWorldPosition(groupPlace);
|
|
814
|
+
const distance = cameraPlace.distanceTo(groupPlace) / camera.zoom;
|
|
815
|
+
const first = childObject(node, 0);
|
|
816
|
+
if (first !== undefined) first.visible = true;
|
|
817
|
+
let level = 1;
|
|
818
|
+
for (; level < count; level++) {
|
|
819
|
+
const object = childObject(node, level);
|
|
820
|
+
const from = distances[level] ?? Number.NaN;
|
|
821
|
+
const reach = object?.visible === true ? from - from * hysteresis : from;
|
|
822
|
+
if (!(distance >= reach)) break;
|
|
823
|
+
const previous = childObject(node, level - 1);
|
|
824
|
+
if (previous !== undefined) previous.visible = false;
|
|
825
|
+
if (object !== undefined) object.visible = true;
|
|
826
|
+
}
|
|
827
|
+
for (; level < count; level++) {
|
|
828
|
+
const object = childObject(node, level);
|
|
829
|
+
if (object !== undefined) object.visible = false;
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const trackDetail = (entry: Entry, node: GroupNode): void => {
|
|
834
|
+
if (node.detail !== null) {
|
|
835
|
+
detailing.add(entry);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
// No longer levels: every child shows again.
|
|
839
|
+
if (detailing.delete(entry)) {
|
|
840
|
+
for (let index = 0; index < node.children.length; index++) {
|
|
841
|
+
const object = childObject(node, index);
|
|
842
|
+
if (object !== undefined) object.visible = true;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
583
845
|
};
|
|
584
846
|
|
|
585
847
|
const build = (node: SceneNode): Entry => {
|
|
@@ -593,6 +855,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
593
855
|
trackInteractive(entry.object, node.interactive);
|
|
594
856
|
// A group following a tracked space shows only once the space is found.
|
|
595
857
|
if (node.follow !== null) entry.object.visible = false;
|
|
858
|
+
trackDetail(entry, node);
|
|
859
|
+
trackTrail(entry, node);
|
|
596
860
|
return entry;
|
|
597
861
|
}
|
|
598
862
|
case 'Mesh': {
|
|
@@ -608,6 +872,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
608
872
|
trackInteractive(object, node.interactive);
|
|
609
873
|
object.castShadow = node.castShadow;
|
|
610
874
|
object.receiveShadow = node.receiveShadow;
|
|
875
|
+
trackEdges(entry, node);
|
|
876
|
+
trackTrail(entry, node);
|
|
611
877
|
return entry;
|
|
612
878
|
}
|
|
613
879
|
case 'Instances': {
|
|
@@ -688,6 +954,28 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
688
954
|
const hosted = options.kit?.shaderGrid(node);
|
|
689
955
|
return { _tag: 'ShaderGrid', node, object: hosted?.object ?? new Group(), hosted };
|
|
690
956
|
}
|
|
957
|
+
case 'Points': {
|
|
958
|
+
const hosted = options.kit?.points?.(node);
|
|
959
|
+
const entry: Entry = { _tag: 'Points', node, object: new Group(), hosted };
|
|
960
|
+
if (hosted !== undefined) entry.object.add(hosted.object);
|
|
961
|
+
writeVec3(entry.object.position, node.position);
|
|
962
|
+
writeVec3(entry.object.scale, node.scale);
|
|
963
|
+
trackRotation(entry, node);
|
|
964
|
+
trackBehaviour(entry, node);
|
|
965
|
+
trackSparkle(entry, node);
|
|
966
|
+
return entry;
|
|
967
|
+
}
|
|
968
|
+
case 'Line': {
|
|
969
|
+
const hosted = options.kit?.line?.(node);
|
|
970
|
+
const entry: Entry = { _tag: 'Line', node, object: new Group(), hosted };
|
|
971
|
+
if (hosted !== undefined) entry.object.add(hosted.object);
|
|
972
|
+
writeVec3(entry.object.position, node.position);
|
|
973
|
+
writeVec3(entry.object.scale, node.scale);
|
|
974
|
+
trackRotation(entry, node);
|
|
975
|
+
trackBehaviour(entry, node);
|
|
976
|
+
trackInteractive(entry.object, node.interactive);
|
|
977
|
+
return entry;
|
|
978
|
+
}
|
|
691
979
|
case 'HemisphereLight': {
|
|
692
980
|
const object = new HemisphereLight();
|
|
693
981
|
writeHemisphereLight(object, node);
|
|
@@ -706,12 +994,22 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
706
994
|
entry.shown = undefined;
|
|
707
995
|
const lent = store.lent(entry.node.id);
|
|
708
996
|
if (lent === undefined) return;
|
|
709
|
-
const shown = lent.parent === null ? lent : lent
|
|
997
|
+
const shown = lent.parent === null ? lent : cloneLoan(lent);
|
|
710
998
|
entry.object.add(shown);
|
|
711
999
|
entry.shown = shown;
|
|
1000
|
+
castShadows(shown, entry.node);
|
|
712
1001
|
startPlayer(entry);
|
|
713
1002
|
};
|
|
714
1003
|
|
|
1004
|
+
/** Drei's `Clone` shadow shorthands: each mesh casts or receives where asked. */
|
|
1005
|
+
const castShadows = (shown: Object3D, node: BorrowedNode): void => {
|
|
1006
|
+
if (!node.castShadow && !node.receiveShadow) return;
|
|
1007
|
+
shown.traverse((part) => {
|
|
1008
|
+
if (part instanceof Mesh && node.castShadow) part.castShadow = true;
|
|
1009
|
+
if (part instanceof Mesh && node.receiveShadow) part.receiveShadow = true;
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
|
|
715
1013
|
const stopPlayer = (entry: Borrowing): void => {
|
|
716
1014
|
if (entry.player === undefined) return;
|
|
717
1015
|
entry.player.mixer.stopAllAction();
|
|
@@ -850,6 +1148,19 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
850
1148
|
const release = (entry: Entry): void => {
|
|
851
1149
|
if (isPlaced(entry)) behaving.delete(entry);
|
|
852
1150
|
spinning.delete(entry);
|
|
1151
|
+
facing.delete(entry);
|
|
1152
|
+
detailing.delete(entry);
|
|
1153
|
+
const lines = edged.get(entry);
|
|
1154
|
+
if (lines !== undefined) {
|
|
1155
|
+
lines.hosted.dispose();
|
|
1156
|
+
edged.delete(entry);
|
|
1157
|
+
}
|
|
1158
|
+
const ribbon = trailing.get(entry);
|
|
1159
|
+
if (ribbon !== undefined) {
|
|
1160
|
+
ribbon.object.removeFromParent();
|
|
1161
|
+
ribbon.dispose();
|
|
1162
|
+
trailing.delete(entry);
|
|
1163
|
+
}
|
|
853
1164
|
moving.delete(entry);
|
|
854
1165
|
reparented.delete(entry);
|
|
855
1166
|
trackInteractive(entry.object, false);
|
|
@@ -861,7 +1172,10 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
861
1172
|
if (entry._tag === 'Instances') entry.object.dispose();
|
|
862
1173
|
// A grid's lines are its own.
|
|
863
1174
|
if (entry._tag === 'Grid') disposeGridLines(entry.helper);
|
|
864
|
-
if (entry._tag === 'ShaderGrid'
|
|
1175
|
+
if (entry._tag === 'ShaderGrid' || entry._tag === 'Line' || entry._tag === 'Points') {
|
|
1176
|
+
entry.hosted?.dispose();
|
|
1177
|
+
}
|
|
1178
|
+
sparkling.delete(entry);
|
|
865
1179
|
if (entry._tag === 'Label') {
|
|
866
1180
|
entry.hosted?.dispose();
|
|
867
1181
|
if (entry.node.material !== null && entry.material !== undefined) {
|
|
@@ -991,6 +1305,12 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
991
1305
|
entry.node = next;
|
|
992
1306
|
if (movesTransform(entry, fields)) retarget(entry, next);
|
|
993
1307
|
if (fields.includes('rotation')) trackRotation(entry, next);
|
|
1308
|
+
if (entry._tag === 'Mesh' && next._tag === 'Mesh' && fields.includes('edges')) {
|
|
1309
|
+
trackEdges(entry, next);
|
|
1310
|
+
}
|
|
1311
|
+
if (entry._tag === 'Mesh' && next._tag === 'Mesh' && fields.includes('trail')) {
|
|
1312
|
+
trackTrail(entry, next);
|
|
1313
|
+
}
|
|
994
1314
|
if (fields.includes('behaviour')) trackBehaviour(entry, next);
|
|
995
1315
|
if (fields.includes('interactive')) trackInteractive(mesh, next.interactive);
|
|
996
1316
|
mesh.castShadow = next.castShadow;
|
|
@@ -1005,6 +1325,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1005
1325
|
if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
|
|
1006
1326
|
if (fields.includes('id')) showLoan(entry);
|
|
1007
1327
|
else if (fields.includes('animation')) startPlayer(entry);
|
|
1328
|
+
if (fields.includes('castShadow') || fields.includes('receiveShadow')) {
|
|
1329
|
+
if (entry.shown !== undefined) castShadows(entry.shown, next);
|
|
1330
|
+
}
|
|
1008
1331
|
return;
|
|
1009
1332
|
}
|
|
1010
1333
|
if (entry._tag === 'Label' && next._tag === 'Label') {
|
|
@@ -1023,6 +1346,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1023
1346
|
if (fields.includes('rotation')) trackRotation(entry, next);
|
|
1024
1347
|
if (fields.includes('behaviour')) trackBehaviour(entry, next);
|
|
1025
1348
|
if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
|
|
1349
|
+
if (fields.includes('detail')) trackDetail(entry, next);
|
|
1350
|
+
if (fields.includes('trail')) trackTrail(entry, next);
|
|
1026
1351
|
if (fields.includes('follow')) {
|
|
1027
1352
|
// Back to its own transform, or hidden until the new space is found.
|
|
1028
1353
|
entry.object.visible = next.follow === null;
|
|
@@ -1059,6 +1384,26 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1059
1384
|
entry.node = next;
|
|
1060
1385
|
return;
|
|
1061
1386
|
}
|
|
1387
|
+
if (entry._tag === 'Points' && next._tag === 'Points') {
|
|
1388
|
+
entry.node = next;
|
|
1389
|
+
if (movesTransform(entry, fields)) retarget(entry, next);
|
|
1390
|
+
if (fields.includes('rotation')) trackRotation(entry, next);
|
|
1391
|
+
if (fields.includes('behaviour')) trackBehaviour(entry, next);
|
|
1392
|
+
if (fields.includes('look')) {
|
|
1393
|
+
entry.hosted?.update(next);
|
|
1394
|
+
trackSparkle(entry, next);
|
|
1395
|
+
}
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (entry._tag === 'Line' && next._tag === 'Line') {
|
|
1399
|
+
entry.node = next;
|
|
1400
|
+
if (movesTransform(entry, fields)) retarget(entry, next);
|
|
1401
|
+
if (fields.includes('rotation')) trackRotation(entry, next);
|
|
1402
|
+
if (fields.includes('behaviour')) trackBehaviour(entry, next);
|
|
1403
|
+
if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
|
|
1404
|
+
if (fields.includes('line')) entry.hosted?.update(next);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1062
1407
|
if (entry._tag === 'ShaderGrid' && next._tag === 'ShaderGrid') {
|
|
1063
1408
|
entry.hosted?.update(entry.node, next);
|
|
1064
1409
|
entry.node = next;
|
|
@@ -1098,6 +1443,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1098
1443
|
applyLens(camera, op.camera);
|
|
1099
1444
|
// A new kind of camera has nothing on screen to travel from.
|
|
1100
1445
|
cameraMotion.aim(camera, op.camera, elapsed, camera !== previous || !animating);
|
|
1446
|
+
cameraShaker.describe(op.camera.shake, elapsed);
|
|
1101
1447
|
fitCamera(camera, viewport);
|
|
1102
1448
|
return;
|
|
1103
1449
|
}
|
|
@@ -1189,6 +1535,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1189
1535
|
gestures: 'page',
|
|
1190
1536
|
views: [],
|
|
1191
1537
|
renderTextures: [],
|
|
1538
|
+
hud: null,
|
|
1192
1539
|
overlays: [],
|
|
1193
1540
|
environment: null,
|
|
1194
1541
|
sky: null,
|
|
@@ -1201,6 +1548,78 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1201
1548
|
children: texture.children,
|
|
1202
1549
|
});
|
|
1203
1550
|
|
|
1551
|
+
/**
|
|
1552
|
+
* The HUD's camera: orthographic in CSS pixels, from in front, aimed so that the HUD's
|
|
1553
|
+
* origin falls `margin` in from the aligned edges (as Drei's `GizmoHelper` places its
|
|
1554
|
+
* group). The camera moves rather than the nodes, so their positions stay the Model's.
|
|
1555
|
+
*/
|
|
1556
|
+
const hudCamera = (hud: Hud): CameraDescription => {
|
|
1557
|
+
const [marginX, marginY] = hud.margin;
|
|
1558
|
+
const [vertical, horizontal] = hud.alignment.split('-');
|
|
1559
|
+
const across = { left: -viewport.width / 2 + marginX, right: viewport.width / 2 - marginX };
|
|
1560
|
+
const up = { top: viewport.height / 2 - marginY, bottom: -viewport.height / 2 + marginY };
|
|
1561
|
+
const x = across[horizontal as keyof typeof across] ?? 0;
|
|
1562
|
+
const y = up[vertical as keyof typeof up] ?? 0;
|
|
1563
|
+
return orthographicCamera({
|
|
1564
|
+
zoom: 1,
|
|
1565
|
+
near: 0.1,
|
|
1566
|
+
far: 2000,
|
|
1567
|
+
position: [-x, -y, 200],
|
|
1568
|
+
lookAt: [-x, -y, 0],
|
|
1569
|
+
});
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
/** The HUD as a scene of its own, sharing the parent's resources. */
|
|
1573
|
+
const hudScene = (hud: Hud, parent: SceneDescription): SceneDescription =>
|
|
1574
|
+
textureScene(
|
|
1575
|
+
{
|
|
1576
|
+
id: 'hud',
|
|
1577
|
+
width: viewport.width,
|
|
1578
|
+
height: viewport.height,
|
|
1579
|
+
camera: hudCamera(hud),
|
|
1580
|
+
background: null,
|
|
1581
|
+
children: hud.children,
|
|
1582
|
+
},
|
|
1583
|
+
parent,
|
|
1584
|
+
);
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* Brings the HUD in line with the description, `parent` the scene it is over: a nested
|
|
1588
|
+
* graph on the same store, created, reconciled, re-aimed or disposed.
|
|
1589
|
+
*/
|
|
1590
|
+
const syncHud = (next: SceneDescription): ReadonlyArray<Op> => {
|
|
1591
|
+
if (next.hud === null) {
|
|
1592
|
+
if (hudHosted === undefined) return [];
|
|
1593
|
+
hudHosted.graph.dispose();
|
|
1594
|
+
hudHosted = undefined;
|
|
1595
|
+
return [{ _tag: 'RemoveHud' }];
|
|
1596
|
+
}
|
|
1597
|
+
const created = hudHosted === undefined;
|
|
1598
|
+
const hosted = hudHosted ?? {
|
|
1599
|
+
graph: createSceneGraph({
|
|
1600
|
+
store,
|
|
1601
|
+
...(options.behaviours === undefined ? {} : { behaviours: options.behaviours }),
|
|
1602
|
+
...(options.kit === undefined ? {} : { kit: options.kit }),
|
|
1603
|
+
}),
|
|
1604
|
+
description: next.hud,
|
|
1605
|
+
parent: next,
|
|
1606
|
+
};
|
|
1607
|
+
if (created) hosted.graph.resize(viewport.width, viewport.height);
|
|
1608
|
+
const result = hosted.graph.reconcile(hudScene(next.hud, next));
|
|
1609
|
+
hosted.description = next.hud;
|
|
1610
|
+
hosted.parent = next;
|
|
1611
|
+
hudHosted = hosted;
|
|
1612
|
+
const ops = result._tag === 'Applied' ? result.ops : [];
|
|
1613
|
+
return created || ops.length > 0 ? [{ _tag: 'UpdateHud', ops }] : [];
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
/** The HUD fitted to a resized canvas: its camera re-aimed at the aligned point. */
|
|
1617
|
+
const refitHud = (): void => {
|
|
1618
|
+
if (hudHosted === undefined) return;
|
|
1619
|
+
hudHosted.graph.resize(viewport.width, viewport.height);
|
|
1620
|
+
hudHosted.graph.reconcile(hudScene(hudHosted.description, hudHosted.parent));
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1204
1623
|
/** A hosted render texture as the loop sees it; none if it is not hosted. */
|
|
1205
1624
|
function hostedTexture(texture: RenderTexture): ReadonlyArray<HostedTexture> {
|
|
1206
1625
|
const hosted = textureGraphs.get(texture.id);
|
|
@@ -1261,6 +1680,10 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1261
1680
|
const checked = indexScene(textureScene(texture, next), knownBehaviours);
|
|
1262
1681
|
if (checked._tag === 'Invalid') return checked;
|
|
1263
1682
|
}
|
|
1683
|
+
if (next.hud !== null) {
|
|
1684
|
+
const checked = indexScene(hudScene(next.hud, next), knownBehaviours);
|
|
1685
|
+
if (checked._tag === 'Invalid') return checked;
|
|
1686
|
+
}
|
|
1264
1687
|
const result = diff(index, next, knownBehaviours);
|
|
1265
1688
|
if (result._tag === 'Invalid') return result;
|
|
1266
1689
|
for (const op of result.ops) apply(op);
|
|
@@ -1294,7 +1717,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1294
1717
|
// Measured after the next drawn frame; a new list needs one.
|
|
1295
1718
|
if (next.measure.join('\u0000') !== measured.join('\u0000')) invalidate();
|
|
1296
1719
|
measured = next.measure;
|
|
1297
|
-
const ops = [...result.ops, ...syncTextures(next)];
|
|
1720
|
+
const ops = [...result.ops, ...syncTextures(next), ...syncHud(next)];
|
|
1298
1721
|
if (ops.length > 0) invalidate();
|
|
1299
1722
|
return { _tag: 'Applied', ops };
|
|
1300
1723
|
};
|
|
@@ -1307,6 +1730,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1307
1730
|
const tick = (elapsedSeconds: number): void => {
|
|
1308
1731
|
elapsed = elapsedSeconds;
|
|
1309
1732
|
for (const texture of textureList) texture.graph.tick(elapsedSeconds);
|
|
1733
|
+
hudHosted?.graph.tick(elapsedSeconds);
|
|
1310
1734
|
for (const entry of spinning) {
|
|
1311
1735
|
if (isPlaced(entry)) {
|
|
1312
1736
|
writeRotation(entry.object.rotation, entry.node.rotation, elapsed);
|
|
@@ -1317,8 +1741,27 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1317
1741
|
behaving.forEach(applyBehaviour);
|
|
1318
1742
|
cameraMotion.step(camera, elapsed);
|
|
1319
1743
|
for (const view of views) viewMotions.get(view.key)?.step(view.camera, elapsed);
|
|
1744
|
+
// Shake on top of wherever the cameras are aimed now.
|
|
1745
|
+
cameraShaker.apply(camera, cameraMotion.target(), elapsed, animating);
|
|
1746
|
+
views.forEach(shakeView);
|
|
1320
1747
|
// Last: a dynamic body's pose is the simulation's, whatever else moved the mesh.
|
|
1321
1748
|
bodyHost.tick(elapsed, animating);
|
|
1749
|
+
// After the camera and everything a billboard may sit on have moved.
|
|
1750
|
+
if (facing.size > 0) {
|
|
1751
|
+
camera.getWorldQuaternion(cameraTurn);
|
|
1752
|
+
facing.forEach(faceCamera);
|
|
1753
|
+
}
|
|
1754
|
+
edged.forEach(refreshEdges);
|
|
1755
|
+
// Reduced motion holds sparkles and stars where they are.
|
|
1756
|
+
if (animating) sparkling.forEach(sparkle);
|
|
1757
|
+
if (trailing.size > 0) {
|
|
1758
|
+
camera.getWorldPosition(eye);
|
|
1759
|
+
trailing.forEach(followTrail);
|
|
1760
|
+
}
|
|
1761
|
+
if (detailing.size > 0) {
|
|
1762
|
+
camera.getWorldPosition(cameraPlace);
|
|
1763
|
+
detailing.forEach(showLevel);
|
|
1764
|
+
}
|
|
1322
1765
|
};
|
|
1323
1766
|
|
|
1324
1767
|
const finishTween = (tween: Tween, entry: Entry): void => {
|
|
@@ -1328,6 +1771,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1328
1771
|
const setAnimating = (next: boolean): void => {
|
|
1329
1772
|
if (next !== animating) invalidate();
|
|
1330
1773
|
for (const texture of textureList) texture.graph.setAnimating(next);
|
|
1774
|
+
hudHosted?.graph.setAnimating(next);
|
|
1331
1775
|
animating = next;
|
|
1332
1776
|
if (next) return;
|
|
1333
1777
|
moving.forEach(finishTween);
|
|
@@ -1342,6 +1786,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1342
1786
|
for (const texture of textureList) texture.graph.dispose();
|
|
1343
1787
|
textureGraphs.clear();
|
|
1344
1788
|
textureList = [];
|
|
1789
|
+
hudHosted?.graph.dispose();
|
|
1790
|
+
hudHosted = undefined;
|
|
1345
1791
|
for (const entry of entries.values()) {
|
|
1346
1792
|
entry.object.removeFromParent();
|
|
1347
1793
|
release(entry);
|
|
@@ -1372,6 +1818,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1372
1818
|
return viewMotions.get(view)?.turned();
|
|
1373
1819
|
},
|
|
1374
1820
|
renderTextures: () => textureList,
|
|
1821
|
+
hud: () => hudHosted,
|
|
1375
1822
|
gestures: () => gestures,
|
|
1376
1823
|
overlays: () => overlayList,
|
|
1377
1824
|
environment: () => environmentDescription,
|
|
@@ -1400,7 +1847,9 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1400
1847
|
renderSettings: () => settings,
|
|
1401
1848
|
needsFrame: () =>
|
|
1402
1849
|
dirty ||
|
|
1850
|
+
trailsMoving() ||
|
|
1403
1851
|
textureList.some((texture) => texture.graph.needsFrame()) ||
|
|
1852
|
+
(hudHosted?.graph.needsFrame() ?? false) ||
|
|
1404
1853
|
(animating &&
|
|
1405
1854
|
(spinning.size > 0 ||
|
|
1406
1855
|
moving.size > 0 ||
|
|
@@ -1408,6 +1857,8 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1408
1857
|
clipsRunning() ||
|
|
1409
1858
|
cameraMotion.moving() ||
|
|
1410
1859
|
viewsMoving() ||
|
|
1860
|
+
shaking() ||
|
|
1861
|
+
sparkling.size > 0 ||
|
|
1411
1862
|
bodyHost.needsFrame())),
|
|
1412
1863
|
drawn: () => {
|
|
1413
1864
|
dirty = false;
|
|
@@ -1435,6 +1886,7 @@ export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph =>
|
|
|
1435
1886
|
viewport = { width, height };
|
|
1436
1887
|
fitCamera(camera, viewport);
|
|
1437
1888
|
hostViews(viewDescriptions);
|
|
1889
|
+
refitHud();
|
|
1438
1890
|
invalidate();
|
|
1439
1891
|
},
|
|
1440
1892
|
dispose,
|