@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,1440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hosted three.js scene graph: applies diff operations to real three.js objects.
|
|
3
|
+
*
|
|
4
|
+
* `reconcile` is atomic: an invalid description is rejected before anything changes.
|
|
5
|
+
* Objects keep their identity across updates; only a change of node kind replaces one.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
AmbientLight,
|
|
11
|
+
Color,
|
|
12
|
+
AnimationClip,
|
|
13
|
+
AnimationMixer,
|
|
14
|
+
type BufferGeometry,
|
|
15
|
+
DirectionalLight,
|
|
16
|
+
Group,
|
|
17
|
+
GridHelper,
|
|
18
|
+
HemisphereLight,
|
|
19
|
+
Mesh,
|
|
20
|
+
type Object3D,
|
|
21
|
+
OrthographicCamera,
|
|
22
|
+
PerspectiveCamera,
|
|
23
|
+
PointLight,
|
|
24
|
+
Scene,
|
|
25
|
+
SpotLight,
|
|
26
|
+
Vector3,
|
|
27
|
+
} from 'three';
|
|
28
|
+
|
|
29
|
+
import { clipRunning, clipTime } from './animation.ts';
|
|
30
|
+
import { writeRotation } from './behaviours.ts';
|
|
31
|
+
import { type BodyCollision, type BodyContactForce, createBodyHost } from './bodies.ts';
|
|
32
|
+
import { type BodySource, instanceSources, nodeSource, placeObject } from './bodySources.ts';
|
|
33
|
+
import { renderSettings, scene as describeScene } from './build.ts';
|
|
34
|
+
import { type CameraMotion, createCameraMotion } from './cameraMotion.ts';
|
|
35
|
+
import type {
|
|
36
|
+
AmbientLightNode,
|
|
37
|
+
BorrowedNode,
|
|
38
|
+
DirectionalLightNode,
|
|
39
|
+
GridNode,
|
|
40
|
+
HemisphereLightNode,
|
|
41
|
+
GroupNode,
|
|
42
|
+
Instance,
|
|
43
|
+
InstancesNode,
|
|
44
|
+
LabelNode,
|
|
45
|
+
MaterialRef,
|
|
46
|
+
ShaderGridNode,
|
|
47
|
+
MeshNode,
|
|
48
|
+
Camera as CameraDescription,
|
|
49
|
+
PointLightNode,
|
|
50
|
+
RenderSettings,
|
|
51
|
+
SceneDescription,
|
|
52
|
+
SceneNode,
|
|
53
|
+
SpotLightNode,
|
|
54
|
+
ContactShadows,
|
|
55
|
+
Environment,
|
|
56
|
+
Gestures,
|
|
57
|
+
Overlay,
|
|
58
|
+
Physics,
|
|
59
|
+
Sky,
|
|
60
|
+
Xr,
|
|
61
|
+
XrSpace,
|
|
62
|
+
RenderTexture,
|
|
63
|
+
Vec3,
|
|
64
|
+
View,
|
|
65
|
+
ViewRect,
|
|
66
|
+
} from './description.ts';
|
|
67
|
+
import { diff, type Field, type Op } from './diff.ts';
|
|
68
|
+
import { contactShadowsEquals, environmentEquals, overlaysEqual, skyEquals } from './equality.ts';
|
|
69
|
+
import { createInstances, fits, type HostInstances, writeInstances } from './instances.ts';
|
|
70
|
+
import type { HostedLabel, LabelEngine, LabelLayout } from './labels.ts';
|
|
71
|
+
import type { HostMaterial } from './materials.ts';
|
|
72
|
+
import {
|
|
73
|
+
type Behaviour,
|
|
74
|
+
type BehaviourPose,
|
|
75
|
+
builtinBehaviours,
|
|
76
|
+
createPose,
|
|
77
|
+
resetPose,
|
|
78
|
+
} from './motion.ts';
|
|
79
|
+
import type { BodyPose, PhysicsEngine } from './physics.ts';
|
|
80
|
+
import type { HostedShaderGrid, HostedSky, RenderKit } from './renderKit.ts';
|
|
81
|
+
import { createResourceStore, type ResourceStore } from './resources.ts';
|
|
82
|
+
import {
|
|
83
|
+
type DescriptionError,
|
|
84
|
+
emptyIndex,
|
|
85
|
+
indexScene,
|
|
86
|
+
type ParentKey,
|
|
87
|
+
type SceneIndex,
|
|
88
|
+
} from './sceneIndex.ts';
|
|
89
|
+
import { applyTween, startTween, type Tween } from './tween.ts';
|
|
90
|
+
|
|
91
|
+
/** The mesh type the kernel hosts. */
|
|
92
|
+
export type HostMesh = Mesh<BufferGeometry, HostMaterial>;
|
|
93
|
+
|
|
94
|
+
type Entry =
|
|
95
|
+
| { readonly _tag: 'Group'; node: GroupNode; readonly object: Group }
|
|
96
|
+
| { readonly _tag: 'Mesh'; node: MeshNode; readonly object: HostMesh }
|
|
97
|
+
// Mutable: growing past capacity swaps in a larger InstancedMesh under the same entry.
|
|
98
|
+
| { readonly _tag: 'Instances'; node: InstancesNode; object: HostInstances }
|
|
99
|
+
// `shown` is the lent object (or a clone of it) inside the wrapper, when one is lent.
|
|
100
|
+
| {
|
|
101
|
+
readonly _tag: 'Borrowed';
|
|
102
|
+
node: BorrowedNode;
|
|
103
|
+
readonly object: Group;
|
|
104
|
+
shown: Object3D | undefined;
|
|
105
|
+
player: Player | undefined;
|
|
106
|
+
}
|
|
107
|
+
// `hosted` is what the label engine drew, inside the wrapper; none without an engine.
|
|
108
|
+
| {
|
|
109
|
+
readonly _tag: 'Label';
|
|
110
|
+
node: LabelNode;
|
|
111
|
+
readonly object: Group;
|
|
112
|
+
readonly hosted: HostedLabel | undefined;
|
|
113
|
+
/** The material lent to the engine (`label({ material })`), held from the store. */
|
|
114
|
+
material: HostMaterial | undefined;
|
|
115
|
+
}
|
|
116
|
+
| { readonly _tag: 'AmbientLight'; node: AmbientLightNode; readonly object: AmbientLight }
|
|
117
|
+
| {
|
|
118
|
+
readonly _tag: 'DirectionalLight';
|
|
119
|
+
node: DirectionalLightNode;
|
|
120
|
+
readonly object: DirectionalLight;
|
|
121
|
+
}
|
|
122
|
+
| { readonly _tag: 'PointLight'; node: PointLightNode; readonly object: PointLight }
|
|
123
|
+
| { readonly _tag: 'SpotLight'; node: SpotLightNode; readonly object: SpotLight }
|
|
124
|
+
| {
|
|
125
|
+
readonly _tag: 'Grid';
|
|
126
|
+
node: GridNode;
|
|
127
|
+
/** Positions the grid; the lines inside are rebuilt when their shape changes. */
|
|
128
|
+
readonly object: Group;
|
|
129
|
+
helper: GridHelper;
|
|
130
|
+
}
|
|
131
|
+
// `hosted` is what the render kit drew; without a kit the grid is an empty group.
|
|
132
|
+
| {
|
|
133
|
+
readonly _tag: 'ShaderGrid';
|
|
134
|
+
node: ShaderGridNode;
|
|
135
|
+
readonly object: Object3D;
|
|
136
|
+
readonly hosted: HostedShaderGrid | undefined;
|
|
137
|
+
}
|
|
138
|
+
| {
|
|
139
|
+
readonly _tag: 'HemisphereLight';
|
|
140
|
+
node: HemisphereLightNode;
|
|
141
|
+
readonly object: HemisphereLight;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Entries with a transform: position, rotation, scale and transitions. */
|
|
145
|
+
/** A clip playing on a borrowed object: its mixer, length and scene-time start. */
|
|
146
|
+
type Borrowing = Extract<Entry, { readonly _tag: 'Borrowed' }>;
|
|
147
|
+
|
|
148
|
+
type Player = {
|
|
149
|
+
readonly mixer: AnimationMixer;
|
|
150
|
+
readonly duration: number;
|
|
151
|
+
readonly startedAt: number;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Descriptions with a transform. */
|
|
155
|
+
type PlacedNode = GroupNode | MeshNode | InstancesNode | BorrowedNode | LabelNode;
|
|
156
|
+
|
|
157
|
+
type Placed = Extract<
|
|
158
|
+
Entry,
|
|
159
|
+
{ readonly _tag: 'Group' | 'Mesh' | 'Instances' | 'Borrowed' | 'Label' }
|
|
160
|
+
>;
|
|
161
|
+
|
|
162
|
+
/** Entries drawing a geometry with a material. */
|
|
163
|
+
type Surfaced = Extract<Entry, { readonly _tag: 'Mesh' | 'Instances' }>;
|
|
164
|
+
|
|
165
|
+
const isPlaced = (entry: Entry): entry is Placed =>
|
|
166
|
+
entry._tag === 'Group' ||
|
|
167
|
+
entry._tag === 'Mesh' ||
|
|
168
|
+
entry._tag === 'Instances' ||
|
|
169
|
+
entry._tag === 'Borrowed' ||
|
|
170
|
+
entry._tag === 'Label';
|
|
171
|
+
|
|
172
|
+
const isSurfaced = (entry: Entry): entry is Surfaced =>
|
|
173
|
+
entry._tag === 'Mesh' || entry._tag === 'Instances';
|
|
174
|
+
|
|
175
|
+
/** A render texture as hosted: its id, size and the nested graph that draws it. */
|
|
176
|
+
export type HostedTexture = {
|
|
177
|
+
readonly id: string;
|
|
178
|
+
readonly graph: SceneGraph;
|
|
179
|
+
readonly width: number;
|
|
180
|
+
readonly height: number;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** A view as hosted: its key, its camera and its rectangle in CSS pixels from the top left. */
|
|
184
|
+
export type HostedView = {
|
|
185
|
+
readonly key: string;
|
|
186
|
+
readonly camera: HostCamera;
|
|
187
|
+
readonly area: {
|
|
188
|
+
readonly x: number;
|
|
189
|
+
readonly y: number;
|
|
190
|
+
readonly width: number;
|
|
191
|
+
readonly height: number;
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export type ReconcileResult =
|
|
196
|
+
| { readonly _tag: 'Applied'; readonly ops: ReadonlyArray<Op> }
|
|
197
|
+
| { readonly _tag: 'Invalid'; readonly error: DescriptionError };
|
|
198
|
+
|
|
199
|
+
export type SceneGraphOptions = {
|
|
200
|
+
/** A store shared with other graphs. When omitted the graph owns a private store. */
|
|
201
|
+
readonly store?: ResourceStore;
|
|
202
|
+
/** Behaviours nodes may name, in addition to the built-in ones (`float`). */
|
|
203
|
+
readonly behaviours?: Readonly<Record<string, Behaviour>>;
|
|
204
|
+
/** Simulates meshes with a `body` when the scene has `physics`; without it they stay put. */
|
|
205
|
+
readonly physics?: PhysicsEngine | undefined;
|
|
206
|
+
/** Draws `label` nodes (distance-field text); without it they are empty. */
|
|
207
|
+
readonly labels?: LabelEngine | undefined;
|
|
208
|
+
/**
|
|
209
|
+
* The renderer's shaded parts (sky, shader grids, contact shadows); without it they are
|
|
210
|
+
* not drawn. `webglKit` from `@opsydyn/effect-three/webgl` for three's WebGL renderer.
|
|
211
|
+
*/
|
|
212
|
+
readonly kit?: RenderKit | undefined;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export type SceneGraph = {
|
|
216
|
+
readonly scene: Scene;
|
|
217
|
+
/** The render kit the graph was made with, for the frame loop's contact shadows. */
|
|
218
|
+
readonly kit: RenderKit | undefined;
|
|
219
|
+
/** The active camera. Changing the description's camera kind replaces it. */
|
|
220
|
+
readonly camera: HostCamera;
|
|
221
|
+
/** The scene's views, fitted to the viewport; empty when `camera` fills the canvas. */
|
|
222
|
+
readonly views: () => ReadonlyArray<HostedView>;
|
|
223
|
+
/** Holds every camera's turning (`autoRotate`) still while a gesture is pressed. */
|
|
224
|
+
readonly holdCameras: (held: boolean) => void;
|
|
225
|
+
/**
|
|
226
|
+
* Where turning has taken the camera of `view` (or, without one, the scene's camera),
|
|
227
|
+
* when it has turned since its description: for an `AutoRotated` event.
|
|
228
|
+
*/
|
|
229
|
+
readonly turned: (view: string | undefined) => Vec3 | undefined;
|
|
230
|
+
/** Render textures, each a nested graph to draw into its texture (by the frame loop). */
|
|
231
|
+
readonly renderTextures: () => ReadonlyArray<HostedTexture>;
|
|
232
|
+
/** Whether the scene claims the pointer's gestures from the page. */
|
|
233
|
+
readonly gestures: () => Gestures;
|
|
234
|
+
/** Points to show HTML at; the frame loop places them after drawing. */
|
|
235
|
+
readonly overlays: () => ReadonlyArray<Overlay>;
|
|
236
|
+
/** Image-based lighting and background; the frame loop applies it. */
|
|
237
|
+
readonly environment: () => Environment | null;
|
|
238
|
+
/** Soft ground shadows; the frame loop draws them. */
|
|
239
|
+
readonly contactShadows: () => ContactShadows | null;
|
|
240
|
+
/** Collisions between bodies since the last call, for the frame loop to report. */
|
|
241
|
+
readonly collisions: () => ReadonlyArray<BodyCollision>;
|
|
242
|
+
/** Contact forces over their bodies' thresholds since the last call. */
|
|
243
|
+
readonly contactForces: () => ReadonlyArray<BodyContactForce>;
|
|
244
|
+
/** How the scene is presented immersively, for the host; `null` if it never is. */
|
|
245
|
+
readonly xr: () => Xr | null;
|
|
246
|
+
/** Keys of the nodes whose bounds the frame loop reports. */
|
|
247
|
+
readonly measured: () => ReadonlyArray<string>;
|
|
248
|
+
/** A label's laid-out size, once its engine has laid it out. */
|
|
249
|
+
readonly labelLayout: (key: string) => LabelLayout | undefined;
|
|
250
|
+
/**
|
|
251
|
+
* Places the groups that follow tracked spaces where `locate` says each space is (in the
|
|
252
|
+
* scene), hiding those it cannot find. A presenting session calls it every frame, and
|
|
253
|
+
* once with nothing found when it ends.
|
|
254
|
+
*/
|
|
255
|
+
readonly follow: (locate: (space: XrSpace) => BodyPose | undefined) => void;
|
|
256
|
+
readonly store: ResourceStore;
|
|
257
|
+
/** Diffs against the last applied description and applies the result. */
|
|
258
|
+
readonly reconcile: (next: SceneDescription) => ReconcileResult;
|
|
259
|
+
/** Advances behaviours to `elapsedSeconds` of scene time. Allocation-free. */
|
|
260
|
+
readonly tick: (elapsedSeconds: number) => void;
|
|
261
|
+
/** Scene time of the last `tick`. */
|
|
262
|
+
readonly elapsedSeconds: () => number;
|
|
263
|
+
/**
|
|
264
|
+
* The scene's current render settings. A new value (by reference) means they changed,
|
|
265
|
+
* so the frame loop can reconfigure the renderer without comparing fields.
|
|
266
|
+
*/
|
|
267
|
+
readonly renderSettings: () => RenderSettings;
|
|
268
|
+
/**
|
|
269
|
+
* While not animating (reduced motion, a paused debugger), transitions land at once and
|
|
270
|
+
* any in flight finish, because frozen scene time would otherwise strand them mid-way.
|
|
271
|
+
*/
|
|
272
|
+
readonly setAnimating: (animating: boolean) => void;
|
|
273
|
+
/**
|
|
274
|
+
* Whether the next frame would draw something new: after any change (reconcile, resize,
|
|
275
|
+
* animation toggle, a shared resource changed elsewhere) or while something animates.
|
|
276
|
+
*/
|
|
277
|
+
readonly needsFrame: () => boolean;
|
|
278
|
+
/** Marks the current state as drawn. The frame loop calls it after each render. */
|
|
279
|
+
readonly drawn: () => void;
|
|
280
|
+
/** Calls `listener` whenever the graph changes in a way that needs a frame. */
|
|
281
|
+
readonly onInvalidated: (listener: () => void) => () => void;
|
|
282
|
+
/** The hosted object for a key. */
|
|
283
|
+
readonly object: (key: string) => Object3D | undefined;
|
|
284
|
+
/** The key of a hosted object. */
|
|
285
|
+
readonly keyOf: (object: Object3D) => string | undefined;
|
|
286
|
+
/** The key of instance `instanceId` of a hosted instances node, if it has one. */
|
|
287
|
+
readonly instanceKeyOf: (object: Object3D, instanceId: number) => string | undefined;
|
|
288
|
+
/**
|
|
289
|
+
* Hosted objects of interactive meshes and groups (picking descends into groups). The
|
|
290
|
+
* array is cached until the set changes and is mutable only because three's
|
|
291
|
+
* `Raycaster.intersectObjects` requires it: don't modify it.
|
|
292
|
+
*/
|
|
293
|
+
readonly interactiveObjects: () => Array<Object3D>;
|
|
294
|
+
/** Fits the camera to a viewport of `width` by `height` CSS pixels. */
|
|
295
|
+
readonly resize: (width: number, height: number) => void;
|
|
296
|
+
/** The last `resize`, in CSS pixels. `1 × 1` until the first resize. */
|
|
297
|
+
readonly viewport: () => { readonly width: number; readonly height: number };
|
|
298
|
+
/** Removes every node and releases its resources. Disposes the store if the graph owns it. */
|
|
299
|
+
readonly dispose: () => void;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const writeVec3 = (
|
|
303
|
+
target: { set: (x: number, y: number, z: number) => unknown },
|
|
304
|
+
v: Vec3,
|
|
305
|
+
): void => {
|
|
306
|
+
target.set(v[0], v[1], v[2]);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
type ShadowFields = {
|
|
310
|
+
readonly castShadow: boolean;
|
|
311
|
+
readonly shadowMapSize: number;
|
|
312
|
+
readonly shadowBias: number;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/** Writes a light's shadow settings. A new map size drops the old map so three reallocates it. */
|
|
316
|
+
const writeShadow = (
|
|
317
|
+
light: DirectionalLight | PointLight | SpotLight,
|
|
318
|
+
fields: ShadowFields,
|
|
319
|
+
): void => {
|
|
320
|
+
light.castShadow = fields.castShadow;
|
|
321
|
+
light.shadow.bias = fields.shadowBias;
|
|
322
|
+
if (light.shadow.mapSize.x !== fields.shadowMapSize) {
|
|
323
|
+
light.shadow.mapSize.set(fields.shadowMapSize, fields.shadowMapSize);
|
|
324
|
+
light.shadow.map?.dispose();
|
|
325
|
+
light.shadow.map = null;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const writeDirectionalLight = (light: DirectionalLight, node: DirectionalLightNode): void => {
|
|
330
|
+
light.color.setHex(node.color);
|
|
331
|
+
light.intensity = node.intensity;
|
|
332
|
+
writeVec3(light.position, node.position);
|
|
333
|
+
writeShadow(light, node);
|
|
334
|
+
const camera = light.shadow.camera;
|
|
335
|
+
if (camera.right !== node.shadowArea) {
|
|
336
|
+
camera.left = -node.shadowArea;
|
|
337
|
+
camera.right = node.shadowArea;
|
|
338
|
+
camera.top = node.shadowArea;
|
|
339
|
+
camera.bottom = -node.shadowArea;
|
|
340
|
+
camera.updateProjectionMatrix();
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Lights are cheap to write in full, so an update rewrites every field from the node.
|
|
345
|
+
const writePointLight = (light: PointLight, node: PointLightNode): void => {
|
|
346
|
+
light.color.setHex(node.color);
|
|
347
|
+
light.intensity = node.intensity;
|
|
348
|
+
writeVec3(light.position, node.position);
|
|
349
|
+
light.distance = node.distance;
|
|
350
|
+
light.decay = node.decay;
|
|
351
|
+
writeShadow(light, node);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const writeSpotLight = (light: SpotLight, node: SpotLightNode): void => {
|
|
355
|
+
light.color.setHex(node.color);
|
|
356
|
+
light.intensity = node.intensity;
|
|
357
|
+
writeVec3(light.position, node.position);
|
|
358
|
+
light.distance = node.distance;
|
|
359
|
+
light.angle = node.angle;
|
|
360
|
+
light.penumbra = node.penumbra;
|
|
361
|
+
light.decay = node.decay;
|
|
362
|
+
writeShadow(light, node);
|
|
363
|
+
// The target is not in the scene graph (it is world space), so three never updates its
|
|
364
|
+
// matrix: do it here, once per change.
|
|
365
|
+
writeVec3(light.target.position, node.target);
|
|
366
|
+
light.target.updateMatrixWorld();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/** Lines for a grid node: three.js's `GridHelper` takes its shape when constructed. */
|
|
370
|
+
const gridLines = (node: GridNode): GridHelper =>
|
|
371
|
+
new GridHelper(node.size, node.divisions, node.centerColor, node.color);
|
|
372
|
+
|
|
373
|
+
const disposeGridLines = (lines: GridHelper): void => {
|
|
374
|
+
lines.removeFromParent();
|
|
375
|
+
lines.dispose();
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const writeHemisphereLight = (light: HemisphereLight, node: HemisphereLightNode): void => {
|
|
379
|
+
light.color.setHex(node.color);
|
|
380
|
+
light.groundColor.setHex(node.groundColor);
|
|
381
|
+
light.intensity = node.intensity;
|
|
382
|
+
writeVec3(light.position, node.position);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/** The three.js camera kinds the kernel hosts. */
|
|
386
|
+
export type HostCamera = PerspectiveCamera | OrthographicCamera;
|
|
387
|
+
|
|
388
|
+
type Viewport = { readonly width: number; readonly height: number };
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Fits a camera's projection to the viewport. Perspective cameras take its aspect ratio;
|
|
392
|
+
* orthographic frustums span it in CSS pixels, scaled by `zoom` (as React Three Fiber's
|
|
393
|
+
* `updateCamera` in `core/utils.tsx` at `d74622e`).
|
|
394
|
+
*/
|
|
395
|
+
const fitCamera = (camera: HostCamera, viewport: Viewport): void => {
|
|
396
|
+
if (camera instanceof OrthographicCamera) {
|
|
397
|
+
camera.left = viewport.width / -2;
|
|
398
|
+
camera.right = viewport.width / 2;
|
|
399
|
+
camera.top = viewport.height / 2;
|
|
400
|
+
camera.bottom = viewport.height / -2;
|
|
401
|
+
} else {
|
|
402
|
+
camera.aspect = viewport.width / viewport.height;
|
|
403
|
+
}
|
|
404
|
+
camera.updateProjectionMatrix();
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
/** A camera of the description's kind: `current` when it already is one, else a new one. */
|
|
408
|
+
const cameraFor = (current: HostCamera, description: CameraDescription): HostCamera => {
|
|
409
|
+
if (description._tag === 'Perspective') {
|
|
410
|
+
return current instanceof PerspectiveCamera ? current : new PerspectiveCamera();
|
|
411
|
+
}
|
|
412
|
+
return current instanceof OrthographicCamera ? current : new OrthographicCamera();
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/** Applies the lens: field of view, near and far. Position, aim and zoom are the pose. */
|
|
416
|
+
const applyLens = (camera: HostCamera, description: CameraDescription): void => {
|
|
417
|
+
if (camera instanceof PerspectiveCamera && description._tag === 'Perspective') {
|
|
418
|
+
camera.fov = description.fov;
|
|
419
|
+
}
|
|
420
|
+
camera.near = description.near;
|
|
421
|
+
camera.far = description.far;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const noInstances: ReadonlyArray<Instance> = [];
|
|
425
|
+
|
|
426
|
+
const emptyScene = describeScene({});
|
|
427
|
+
|
|
428
|
+
/** Creates an empty scene graph. */
|
|
429
|
+
export const createSceneGraph = (options: SceneGraphOptions = {}): SceneGraph => {
|
|
430
|
+
const store = options.store ?? createResourceStore();
|
|
431
|
+
const ownsStore = options.store === undefined;
|
|
432
|
+
const scene = new Scene();
|
|
433
|
+
let camera: HostCamera = new PerspectiveCamera();
|
|
434
|
+
let views: ReadonlyArray<HostedView> = [];
|
|
435
|
+
const textureGraphs = new Map<
|
|
436
|
+
string,
|
|
437
|
+
{ readonly graph: SceneGraph; description: RenderTexture }
|
|
438
|
+
>();
|
|
439
|
+
let textureList: ReadonlyArray<HostedTexture> = [];
|
|
440
|
+
let viewDescriptions: ReadonlyArray<View> = [];
|
|
441
|
+
let gestures: Gestures = emptyScene.gestures;
|
|
442
|
+
let overlayList: ReadonlyArray<Overlay> = emptyScene.overlays;
|
|
443
|
+
let environmentDescription: Environment | null = emptyScene.environment;
|
|
444
|
+
let skyDescription: Sky | null = emptyScene.sky;
|
|
445
|
+
let contactDescription: ContactShadows | null = emptyScene.contactShadows;
|
|
446
|
+
let xrDescription: Xr | null = emptyScene.xr;
|
|
447
|
+
let measured: ReadonlyArray<string> = emptyScene.measure;
|
|
448
|
+
let hostedSky: HostedSky | undefined;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Shows, updates or removes the sky, drawn by the render kit (none without one). It
|
|
452
|
+
* belongs to the scene, never picked.
|
|
453
|
+
*/
|
|
454
|
+
const applySky = (description: Sky | null): void => {
|
|
455
|
+
if (description === null) {
|
|
456
|
+
removeSky();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (hostedSky === undefined) {
|
|
460
|
+
hostedSky = options.kit?.sky();
|
|
461
|
+
if (hostedSky !== undefined) scene.add(hostedSky.object);
|
|
462
|
+
}
|
|
463
|
+
hostedSky?.update(description);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
function removeSky(): void {
|
|
467
|
+
if (hostedSky === undefined) return;
|
|
468
|
+
hostedSky.object.removeFromParent();
|
|
469
|
+
hostedSky.dispose();
|
|
470
|
+
hostedSky = undefined;
|
|
471
|
+
}
|
|
472
|
+
const cameraMotion = createCameraMotion();
|
|
473
|
+
/** Each view's camera motion, by the view's key. */
|
|
474
|
+
const viewMotions = new Map<string, CameraMotion>();
|
|
475
|
+
const viewsMoving = (): boolean => {
|
|
476
|
+
for (const motion of viewMotions.values()) if (motion.moving()) return true;
|
|
477
|
+
return false;
|
|
478
|
+
};
|
|
479
|
+
const bodyHost = createBodyHost(options.physics, scene);
|
|
480
|
+
|
|
481
|
+
/** Every body the hosted nodes describe: one per bodied node, one per bodied instance. */
|
|
482
|
+
function* bodySources(): Generator<BodySource> {
|
|
483
|
+
for (const [key, entry] of entries) {
|
|
484
|
+
if (entry._tag === 'Mesh' || entry._tag === 'Group' || entry._tag === 'Borrowed') {
|
|
485
|
+
const source = nodeSource(key, entry.node, entry.object);
|
|
486
|
+
if (source !== undefined) yield source;
|
|
487
|
+
} else if (entry._tag === 'Instances') {
|
|
488
|
+
yield* instanceSources(key, entry.node, entry, entry.object);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
let physicsDescription: Physics | null = null;
|
|
493
|
+
|
|
494
|
+
/** The view's rectangle in CSS pixels, from its fractional rect and the viewport. */
|
|
495
|
+
const areaOf = (rect: ViewRect): HostedView['area'] => ({
|
|
496
|
+
x: rect.x * viewport.width,
|
|
497
|
+
y: rect.y * viewport.height,
|
|
498
|
+
width: Math.max(1, rect.width * viewport.width),
|
|
499
|
+
height: Math.max(1, rect.height * viewport.height),
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
/** Hosts one described view, reusing `previous` when its kind is unchanged. */
|
|
503
|
+
const hostView = (description: View, previous: HostCamera | undefined): HostedView => {
|
|
504
|
+
const hostedCamera = cameraFor(previous ?? new PerspectiveCamera(), description.camera);
|
|
505
|
+
applyLens(hostedCamera, description.camera);
|
|
506
|
+
const motion = viewMotions.get(description.key) ?? createCameraMotion();
|
|
507
|
+
viewMotions.set(description.key, motion);
|
|
508
|
+
// As the scene's camera: a new kind of camera has nothing on screen to travel from.
|
|
509
|
+
motion.aim(hostedCamera, description.camera, elapsed, hostedCamera !== previous || !animating);
|
|
510
|
+
const area = areaOf(description.rect);
|
|
511
|
+
fitCamera(hostedCamera, area);
|
|
512
|
+
return { key: description.key, camera: hostedCamera, area };
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/** Hosts the described views, reusing each key's camera when its kind is unchanged. */
|
|
516
|
+
const hostViews = (described: ReadonlyArray<View>): void => {
|
|
517
|
+
const previous = new Map(views.map((hosted) => [hosted.key, hosted.camera]));
|
|
518
|
+
viewDescriptions = described;
|
|
519
|
+
views = described.map((description) => hostView(description, previous.get(description.key)));
|
|
520
|
+
for (const key of viewMotions.keys()) {
|
|
521
|
+
if (!described.some((description) => description.key === key)) viewMotions.delete(key);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
const entries = new Map<string, Entry>();
|
|
525
|
+
const keys = new WeakMap<Object3D, string>();
|
|
526
|
+
const spinning = new Set<Entry>();
|
|
527
|
+
const playing = new Set<Borrowing>();
|
|
528
|
+
const registry: Readonly<Record<string, Behaviour>> = {
|
|
529
|
+
...builtinBehaviours,
|
|
530
|
+
...options.behaviours,
|
|
531
|
+
};
|
|
532
|
+
const knownBehaviours: ReadonlySet<string> = new Set(Object.keys(registry));
|
|
533
|
+
// Each behaving entry keeps the pose last applied, so it can be taken off again.
|
|
534
|
+
const behaving = new Map<
|
|
535
|
+
Placed,
|
|
536
|
+
{ readonly behaviour: Behaviour; readonly pose: BehaviourPose }
|
|
537
|
+
>();
|
|
538
|
+
const moving = new Map<Entry, Tween>();
|
|
539
|
+
const reparented = new Set<Entry>();
|
|
540
|
+
const worldPosition = new Vector3();
|
|
541
|
+
const interactive = new Set<Object3D>();
|
|
542
|
+
let interactiveList: Array<Object3D> | undefined;
|
|
543
|
+
let index: SceneIndex = emptyIndex;
|
|
544
|
+
let elapsed = 0;
|
|
545
|
+
let animating = true;
|
|
546
|
+
let dirty = true;
|
|
547
|
+
const invalidationListeners = new Set<() => void>();
|
|
548
|
+
const invalidate = (): void => {
|
|
549
|
+
dirty = true;
|
|
550
|
+
for (const listener of invalidationListeners) listener();
|
|
551
|
+
};
|
|
552
|
+
let settings: RenderSettings = renderSettings();
|
|
553
|
+
let viewport = { width: 1, height: 1 };
|
|
554
|
+
|
|
555
|
+
const unsubscribeGeometry = store.onGeometryReplaced(({ previous, next }) => {
|
|
556
|
+
for (const entry of entries.values()) {
|
|
557
|
+
if (isSurfaced(entry) && entry.object.geometry === previous) entry.object.geometry = next;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
const unsubscribeShared = store.onSharedChanged(invalidate);
|
|
561
|
+
const unsubscribeMaterial = store.onMaterialReplaced(({ previous, next }) => {
|
|
562
|
+
for (const entry of entries.values()) {
|
|
563
|
+
if (isSurfaced(entry) && entry.object.material === previous) entry.object.material = next;
|
|
564
|
+
if (entry._tag === 'Label' && entry.material === previous) {
|
|
565
|
+
entry.material = next;
|
|
566
|
+
entry.hosted?.setMaterial(next);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const trackInteractive = (object: Object3D, enabled: boolean): void => {
|
|
572
|
+
const changed = enabled ? !interactive.has(object) : interactive.has(object);
|
|
573
|
+
if (!changed) return;
|
|
574
|
+
if (enabled) interactive.add(object);
|
|
575
|
+
else interactive.delete(object);
|
|
576
|
+
interactiveList = undefined;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const trackRotation = (entry: Entry, node: PlacedNode): void => {
|
|
580
|
+
writeRotation(entry.object.rotation, node.rotation, elapsed);
|
|
581
|
+
if (node.rotation._tag === 'Spin') spinning.add(entry);
|
|
582
|
+
else spinning.delete(entry);
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const build = (node: SceneNode): Entry => {
|
|
586
|
+
switch (node._tag) {
|
|
587
|
+
case 'Group': {
|
|
588
|
+
const entry: Entry = { _tag: 'Group', node, object: new Group() };
|
|
589
|
+
writeVec3(entry.object.position, node.position);
|
|
590
|
+
writeVec3(entry.object.scale, node.scale);
|
|
591
|
+
trackRotation(entry, node);
|
|
592
|
+
trackBehaviour(entry, node);
|
|
593
|
+
trackInteractive(entry.object, node.interactive);
|
|
594
|
+
// A group following a tracked space shows only once the space is found.
|
|
595
|
+
if (node.follow !== null) entry.object.visible = false;
|
|
596
|
+
return entry;
|
|
597
|
+
}
|
|
598
|
+
case 'Mesh': {
|
|
599
|
+
const object: HostMesh = new Mesh(
|
|
600
|
+
store.acquireGeometry(node.geometry),
|
|
601
|
+
store.acquireMaterial(node.material),
|
|
602
|
+
);
|
|
603
|
+
const entry: Entry = { _tag: 'Mesh', node, object };
|
|
604
|
+
writeVec3(object.position, node.position);
|
|
605
|
+
writeVec3(object.scale, node.scale);
|
|
606
|
+
trackRotation(entry, node);
|
|
607
|
+
trackBehaviour(entry, node);
|
|
608
|
+
trackInteractive(object, node.interactive);
|
|
609
|
+
object.castShadow = node.castShadow;
|
|
610
|
+
object.receiveShadow = node.receiveShadow;
|
|
611
|
+
return entry;
|
|
612
|
+
}
|
|
613
|
+
case 'Instances': {
|
|
614
|
+
const object = createInstances(
|
|
615
|
+
store.acquireGeometry(node.geometry),
|
|
616
|
+
store.acquireMaterial(node.material),
|
|
617
|
+
node.instances.length,
|
|
618
|
+
);
|
|
619
|
+
writeInstances(object, noInstances, node.instances);
|
|
620
|
+
const entry: Entry = { _tag: 'Instances', node, object };
|
|
621
|
+
writeVec3(object.position, node.position);
|
|
622
|
+
writeVec3(object.scale, node.scale);
|
|
623
|
+
trackRotation(entry, node);
|
|
624
|
+
trackBehaviour(entry, node);
|
|
625
|
+
trackInteractive(object, node.interactive);
|
|
626
|
+
object.castShadow = node.castShadow;
|
|
627
|
+
object.receiveShadow = node.receiveShadow;
|
|
628
|
+
return entry;
|
|
629
|
+
}
|
|
630
|
+
case 'Borrowed': {
|
|
631
|
+
const entry: Borrowing = {
|
|
632
|
+
_tag: 'Borrowed',
|
|
633
|
+
node,
|
|
634
|
+
object: new Group(),
|
|
635
|
+
shown: undefined,
|
|
636
|
+
player: undefined,
|
|
637
|
+
};
|
|
638
|
+
writeVec3(entry.object.position, node.position);
|
|
639
|
+
writeVec3(entry.object.scale, node.scale);
|
|
640
|
+
trackRotation(entry, node);
|
|
641
|
+
trackBehaviour(entry, node);
|
|
642
|
+
trackInteractive(entry.object, node.interactive);
|
|
643
|
+
showLoan(entry);
|
|
644
|
+
return entry;
|
|
645
|
+
}
|
|
646
|
+
case 'Label': {
|
|
647
|
+
// A renderer that cannot draw the engine's labels (WebGPU and troika) gets none.
|
|
648
|
+
const hosted =
|
|
649
|
+
options.kit?.drawsLabels === false ? undefined : options.labels?.create(node, invalidate);
|
|
650
|
+
const material = node.material === null ? undefined : store.acquireMaterial(node.material);
|
|
651
|
+
hosted?.setMaterial(material);
|
|
652
|
+
const entry: Entry = { _tag: 'Label', node, object: new Group(), hosted, material };
|
|
653
|
+
if (hosted !== undefined) entry.object.add(hosted.object);
|
|
654
|
+
writeVec3(entry.object.position, node.position);
|
|
655
|
+
writeVec3(entry.object.scale, node.scale);
|
|
656
|
+
trackRotation(entry, node);
|
|
657
|
+
trackBehaviour(entry, node);
|
|
658
|
+
trackInteractive(entry.object, node.interactive);
|
|
659
|
+
return entry;
|
|
660
|
+
}
|
|
661
|
+
case 'AmbientLight':
|
|
662
|
+
return { _tag: 'AmbientLight', node, object: new AmbientLight(node.color, node.intensity) };
|
|
663
|
+
case 'DirectionalLight': {
|
|
664
|
+
const object = new DirectionalLight();
|
|
665
|
+
writeDirectionalLight(object, node);
|
|
666
|
+
return { _tag: 'DirectionalLight', node, object };
|
|
667
|
+
}
|
|
668
|
+
case 'PointLight': {
|
|
669
|
+
const object = new PointLight();
|
|
670
|
+
writePointLight(object, node);
|
|
671
|
+
return { _tag: 'PointLight', node, object };
|
|
672
|
+
}
|
|
673
|
+
case 'SpotLight': {
|
|
674
|
+
const object = new SpotLight();
|
|
675
|
+
writeSpotLight(object, node);
|
|
676
|
+
return { _tag: 'SpotLight', node, object };
|
|
677
|
+
}
|
|
678
|
+
case 'Grid': {
|
|
679
|
+
const object = new Group();
|
|
680
|
+
const helper = gridLines(node);
|
|
681
|
+
object.add(helper);
|
|
682
|
+
writeVec3(object.position, node.position);
|
|
683
|
+
return { _tag: 'Grid', node, object, helper };
|
|
684
|
+
}
|
|
685
|
+
case 'ShaderGrid': {
|
|
686
|
+
const hosted = options.kit?.shaderGrid(node);
|
|
687
|
+
return { _tag: 'ShaderGrid', node, object: hosted?.object ?? new Group(), hosted };
|
|
688
|
+
}
|
|
689
|
+
case 'HemisphereLight': {
|
|
690
|
+
const object = new HemisphereLight();
|
|
691
|
+
writeHemisphereLight(object, node);
|
|
692
|
+
return { _tag: 'HemisphereLight', node, object };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Shows what is lent under the entry's id: the object itself, or a clone when another
|
|
699
|
+
* borrowed node already shows it (an object can have only one parent).
|
|
700
|
+
*/
|
|
701
|
+
const showLoan = (entry: Borrowing): void => {
|
|
702
|
+
stopPlayer(entry);
|
|
703
|
+
entry.shown?.removeFromParent();
|
|
704
|
+
entry.shown = undefined;
|
|
705
|
+
const lent = store.lent(entry.node.id);
|
|
706
|
+
if (lent === undefined) return;
|
|
707
|
+
const shown = lent.parent === null ? lent : lent.clone();
|
|
708
|
+
entry.object.add(shown);
|
|
709
|
+
entry.shown = shown;
|
|
710
|
+
startPlayer(entry);
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const stopPlayer = (entry: Borrowing): void => {
|
|
714
|
+
if (entry.player === undefined) return;
|
|
715
|
+
entry.player.mixer.stopAllAction();
|
|
716
|
+
if (entry.shown !== undefined) entry.player.mixer.uncacheRoot(entry.shown);
|
|
717
|
+
entry.player = undefined;
|
|
718
|
+
playing.delete(entry);
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
/** Starts the node's clip on what it shows, from now in scene time, if both exist. */
|
|
722
|
+
const startPlayer = (entry: Borrowing): void => {
|
|
723
|
+
stopPlayer(entry);
|
|
724
|
+
const animation = entry.node.animation;
|
|
725
|
+
const shown = entry.shown;
|
|
726
|
+
if (animation === null || shown === undefined) return;
|
|
727
|
+
const clip = AnimationClip.findByName(shown.animations, animation.clip);
|
|
728
|
+
if (clip === null) return;
|
|
729
|
+
const mixer = new AnimationMixer(shown);
|
|
730
|
+
mixer.clipAction(clip).play();
|
|
731
|
+
entry.player = { mixer, duration: clip.duration, startedAt: elapsed };
|
|
732
|
+
playing.add(entry);
|
|
733
|
+
pose(entry);
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
/** Shows the clip's pose for the current scene time. */
|
|
737
|
+
const pose = (entry: Borrowing): void => {
|
|
738
|
+
const animation = entry.node.animation;
|
|
739
|
+
if (entry.player === undefined || animation === null) return;
|
|
740
|
+
const since = elapsed - entry.player.startedAt;
|
|
741
|
+
entry.player.mixer.setTime(clipTime(animation, entry.player.duration, since));
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
/** Whether any clip still changes its pose, so frames are needed. */
|
|
745
|
+
const clipsRunning = (): boolean => {
|
|
746
|
+
for (const entry of playing) {
|
|
747
|
+
const animation = entry.node.animation;
|
|
748
|
+
if (entry.player === undefined || animation === null) continue;
|
|
749
|
+
const since = elapsed - entry.player.startedAt;
|
|
750
|
+
if (clipRunning(animation, entry.player.duration, since)) return true;
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
/** Re-applies owned materials that use a texture whose loan changed. */
|
|
756
|
+
const refreshTexture = (id: string): void => {
|
|
757
|
+
for (const entry of entries.values()) {
|
|
758
|
+
if (!isSurfaced(entry)) continue;
|
|
759
|
+
const material = entry.node.material;
|
|
760
|
+
if (material._tag !== 'SharedMaterial' && 'map' in material && material.map === id) {
|
|
761
|
+
store.updateOwnedMaterial(entry.object.material, material);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
/** Rebuilds owned text whose font's loan changed; shared text the store rebuilds. */
|
|
767
|
+
const refreshFont = (id: string): void => {
|
|
768
|
+
for (const entry of entries.values()) {
|
|
769
|
+
if (!isSurfaced(entry)) continue;
|
|
770
|
+
const geometry = entry.node.geometry;
|
|
771
|
+
if (geometry._tag === 'Text' && geometry.font === id) {
|
|
772
|
+
store.releaseGeometry(geometry, entry.object.geometry);
|
|
773
|
+
entry.object.geometry = store.acquireGeometry(geometry);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
/** Shows a newly lent (or withdrawn) geometry on the meshes that name it. */
|
|
779
|
+
const refreshLentGeometry = (id: string): void => {
|
|
780
|
+
for (const entry of entries.values()) {
|
|
781
|
+
if (!isSurfaced(entry)) continue;
|
|
782
|
+
const geometry = entry.node.geometry;
|
|
783
|
+
if (geometry._tag === 'LentGeometry' && geometry.id === id) {
|
|
784
|
+
entry.object.geometry = store.acquireGeometry(geometry);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const unsubscribeLoans = store.onLoanChanged((loan) => {
|
|
790
|
+
if (loan.kind === 'texture') refreshTexture(loan.id);
|
|
791
|
+
else if (loan.kind === 'font') refreshFont(loan.id);
|
|
792
|
+
else if (loan.kind === 'geometry') refreshLentGeometry(loan.id);
|
|
793
|
+
else {
|
|
794
|
+
for (const entry of entries.values()) {
|
|
795
|
+
if (entry._tag === 'Borrowed' && entry.node.id === loan.id) showLoan(entry);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
// A model or font arriving changes what bodies are made of: remake their colliders.
|
|
799
|
+
if (loan.kind !== 'texture') bodyHost.sync(physicsDescription, bodySources());
|
|
800
|
+
invalidate();
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
/** Removes the last behaviour pose from the object, leaving its base transform. */
|
|
804
|
+
const liftPose = (entry: Entry): void => {
|
|
805
|
+
const motion = isPlaced(entry) ? behaving.get(entry) : undefined;
|
|
806
|
+
if (motion === undefined) return;
|
|
807
|
+
entry.object.position.sub(motion.pose.offset);
|
|
808
|
+
const { x, y, z } = motion.pose.scale;
|
|
809
|
+
if (x !== 0 && y !== 0 && z !== 0) entry.object.scale.divide(motion.pose.scale);
|
|
810
|
+
resetPose(motion.pose);
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
/** Starts, changes or stops the node's behaviour. */
|
|
814
|
+
const trackBehaviour = (entry: Placed, node: PlacedNode): void => {
|
|
815
|
+
liftPose(entry);
|
|
816
|
+
behaving.delete(entry);
|
|
817
|
+
if (node.behaviour === null) {
|
|
818
|
+
trackRotation(entry, node);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const behaviour = registry[node.behaviour.name];
|
|
822
|
+
if (behaviour !== undefined) behaving.set(entry, { behaviour, pose: createPose() });
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
/** Composes the node's behaviour pose on top of its base transform for this frame. */
|
|
826
|
+
const applyBehaviour = (
|
|
827
|
+
motion: { readonly behaviour: Behaviour; readonly pose: BehaviourPose },
|
|
828
|
+
entry: Placed,
|
|
829
|
+
): void => {
|
|
830
|
+
const node = entry.node;
|
|
831
|
+
if (node.behaviour === null) return;
|
|
832
|
+
const object = entry.object;
|
|
833
|
+
// Tweens have written this frame's base already; otherwise the base is the target.
|
|
834
|
+
if (!moving.has(entry)) {
|
|
835
|
+
writeVec3(object.position, node.position);
|
|
836
|
+
writeVec3(object.scale, node.scale);
|
|
837
|
+
}
|
|
838
|
+
writeRotation(object.rotation, node.rotation, elapsed);
|
|
839
|
+
resetPose(motion.pose);
|
|
840
|
+
motion.behaviour(motion.pose, elapsed, node.behaviour.params);
|
|
841
|
+
object.position.add(motion.pose.offset);
|
|
842
|
+
object.scale.multiply(motion.pose.scale);
|
|
843
|
+
object.rotation.x += motion.pose.rotation.x;
|
|
844
|
+
object.rotation.y += motion.pose.rotation.y;
|
|
845
|
+
object.rotation.z += motion.pose.rotation.z;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const release = (entry: Entry): void => {
|
|
849
|
+
if (isPlaced(entry)) behaving.delete(entry);
|
|
850
|
+
spinning.delete(entry);
|
|
851
|
+
moving.delete(entry);
|
|
852
|
+
reparented.delete(entry);
|
|
853
|
+
trackInteractive(entry.object, false);
|
|
854
|
+
if (isSurfaced(entry)) {
|
|
855
|
+
store.releaseGeometry(entry.node.geometry, entry.object.geometry);
|
|
856
|
+
store.releaseMaterial(entry.node.material, entry.object.material);
|
|
857
|
+
}
|
|
858
|
+
// Frees the instance buffers on the GPU; geometry and material belong to the store.
|
|
859
|
+
if (entry._tag === 'Instances') entry.object.dispose();
|
|
860
|
+
// A grid's lines are its own.
|
|
861
|
+
if (entry._tag === 'Grid') disposeGridLines(entry.helper);
|
|
862
|
+
if (entry._tag === 'ShaderGrid') entry.hosted?.dispose();
|
|
863
|
+
if (entry._tag === 'Label') {
|
|
864
|
+
entry.hosted?.dispose();
|
|
865
|
+
if (entry.node.material !== null && entry.material !== undefined) {
|
|
866
|
+
store.releaseMaterial(entry.node.material, entry.material);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// A borrowed object belongs to the application: detach it, never dispose it.
|
|
870
|
+
if (entry._tag === 'Borrowed') {
|
|
871
|
+
stopPlayer(entry);
|
|
872
|
+
entry.shown?.removeFromParent();
|
|
873
|
+
entry.shown = undefined;
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Moves an instances entry into a larger InstancedMesh: same place in the tree, same
|
|
879
|
+
* transform and key, geometry and material carried over, every slot rewritten.
|
|
880
|
+
*/
|
|
881
|
+
const growInstances = (
|
|
882
|
+
entry: Extract<Entry, { readonly _tag: 'Instances' }>,
|
|
883
|
+
next: InstancesNode,
|
|
884
|
+
): void => {
|
|
885
|
+
const old = entry.object;
|
|
886
|
+
const grown = createInstances(old.geometry, old.material, next.instances.length);
|
|
887
|
+
grown.position.copy(old.position);
|
|
888
|
+
grown.quaternion.copy(old.quaternion);
|
|
889
|
+
grown.scale.copy(old.scale);
|
|
890
|
+
grown.castShadow = old.castShadow;
|
|
891
|
+
grown.receiveShadow = old.receiveShadow;
|
|
892
|
+
grown.name = old.name;
|
|
893
|
+
const parent = old.parent;
|
|
894
|
+
if (parent !== null) {
|
|
895
|
+
const slot = parent.children.indexOf(old);
|
|
896
|
+
old.removeFromParent();
|
|
897
|
+
parent.add(grown);
|
|
898
|
+
parent.children.pop();
|
|
899
|
+
parent.children.splice(slot, 0, grown);
|
|
900
|
+
}
|
|
901
|
+
const key = keys.get(old);
|
|
902
|
+
keys.delete(old);
|
|
903
|
+
if (key !== undefined) keys.set(grown, key);
|
|
904
|
+
const wasInteractive = interactive.has(old);
|
|
905
|
+
trackInteractive(old, false);
|
|
906
|
+
trackInteractive(grown, wasInteractive);
|
|
907
|
+
// Keep each slot as it is shown (a simulated pose, say) and write only what changed.
|
|
908
|
+
const kept = Math.min(entry.node.instances.length, next.instances.length);
|
|
909
|
+
grown.instanceMatrix.array.set(old.instanceMatrix.array.subarray(0, kept * 16));
|
|
910
|
+
if (old.instanceColor !== null && grown.instanceColor !== null) {
|
|
911
|
+
grown.instanceColor.array.set(old.instanceColor.array.subarray(0, kept * 3));
|
|
912
|
+
}
|
|
913
|
+
old.dispose();
|
|
914
|
+
entry.object = grown;
|
|
915
|
+
writeInstances(grown, entry.node.instances.slice(0, kept), next.instances);
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Moves the object towards a new position and scale: at once for `Instant` or while not
|
|
920
|
+
* animating, otherwise by restarting the tween from the current rendered pose at the last
|
|
921
|
+
* frame's time.
|
|
922
|
+
*/
|
|
923
|
+
const retarget = (entry: Entry, node: PlacedNode): void => {
|
|
924
|
+
liftPose(entry);
|
|
925
|
+
if (!animating || node.transition._tag === 'Instant') {
|
|
926
|
+
moving.delete(entry);
|
|
927
|
+
writeVec3(entry.object.position, node.position);
|
|
928
|
+
writeVec3(entry.object.scale, node.scale);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
moving.set(entry, startTween(entry.object, node, elapsed, node.transition));
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const movesTransform = (entry: Entry, fields: ReadonlyArray<Field>): boolean =>
|
|
935
|
+
fields.includes('position') ||
|
|
936
|
+
fields.includes('scale') ||
|
|
937
|
+
(fields.includes('transition') && moving.has(entry));
|
|
938
|
+
|
|
939
|
+
/** Lends a label its next material: updated in place when both are its own, as a mesh's. */
|
|
940
|
+
const relendLabelMaterial = (
|
|
941
|
+
entry: Extract<Entry, { readonly _tag: 'Label' }>,
|
|
942
|
+
next: MaterialRef | null,
|
|
943
|
+
): void => {
|
|
944
|
+
const prev = entry.node.material;
|
|
945
|
+
if (
|
|
946
|
+
prev !== null &&
|
|
947
|
+
next !== null &&
|
|
948
|
+
entry.material !== undefined &&
|
|
949
|
+
prev._tag !== 'SharedMaterial' &&
|
|
950
|
+
next._tag !== 'SharedMaterial' &&
|
|
951
|
+
store.updateOwnedMaterial(entry.material, next)
|
|
952
|
+
) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (prev !== null && entry.material !== undefined) store.releaseMaterial(prev, entry.material);
|
|
956
|
+
entry.material = next === null ? undefined : store.acquireMaterial(next);
|
|
957
|
+
entry.hosted?.setMaterial(entry.material);
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
const update = (entry: Entry, next: SceneNode, fields: ReadonlyArray<Field>): void => {
|
|
961
|
+
if (entry._tag === 'Instances' && next._tag === 'Instances' && fields.includes('instances')) {
|
|
962
|
+
if (fits(entry.object, next.instances.length)) {
|
|
963
|
+
writeInstances(entry.object, entry.node.instances, next.instances);
|
|
964
|
+
} else {
|
|
965
|
+
growInstances(entry, next);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (
|
|
969
|
+
(entry._tag === 'Mesh' && next._tag === 'Mesh') ||
|
|
970
|
+
(entry._tag === 'Instances' && next._tag === 'Instances')
|
|
971
|
+
) {
|
|
972
|
+
const prev = entry.node;
|
|
973
|
+
const mesh = entry.object;
|
|
974
|
+
for (const field of fields) {
|
|
975
|
+
if (field === 'geometry') {
|
|
976
|
+
store.releaseGeometry(prev.geometry, mesh.geometry);
|
|
977
|
+
mesh.geometry = store.acquireGeometry(next.geometry);
|
|
978
|
+
} else if (field === 'material') {
|
|
979
|
+
const updatedInPlace =
|
|
980
|
+
prev.material._tag !== 'SharedMaterial' &&
|
|
981
|
+
next.material._tag !== 'SharedMaterial' &&
|
|
982
|
+
store.updateOwnedMaterial(mesh.material, next.material);
|
|
983
|
+
if (!updatedInPlace) {
|
|
984
|
+
store.releaseMaterial(prev.material, mesh.material);
|
|
985
|
+
mesh.material = store.acquireMaterial(next.material);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
entry.node = next;
|
|
990
|
+
if (movesTransform(entry, fields)) retarget(entry, next);
|
|
991
|
+
if (fields.includes('rotation')) trackRotation(entry, next);
|
|
992
|
+
if (fields.includes('behaviour')) trackBehaviour(entry, next);
|
|
993
|
+
if (fields.includes('interactive')) trackInteractive(mesh, next.interactive);
|
|
994
|
+
mesh.castShadow = next.castShadow;
|
|
995
|
+
mesh.receiveShadow = next.receiveShadow;
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (entry._tag === 'Borrowed' && next._tag === 'Borrowed') {
|
|
999
|
+
entry.node = next;
|
|
1000
|
+
if (movesTransform(entry, fields)) retarget(entry, next);
|
|
1001
|
+
if (fields.includes('rotation')) trackRotation(entry, next);
|
|
1002
|
+
if (fields.includes('behaviour')) trackBehaviour(entry, next);
|
|
1003
|
+
if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
|
|
1004
|
+
if (fields.includes('id')) showLoan(entry);
|
|
1005
|
+
else if (fields.includes('animation')) startPlayer(entry);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (entry._tag === 'Label' && next._tag === 'Label') {
|
|
1009
|
+
if (fields.includes('material')) relendLabelMaterial(entry, next.material);
|
|
1010
|
+
entry.node = next;
|
|
1011
|
+
if (movesTransform(entry, fields)) retarget(entry, next);
|
|
1012
|
+
if (fields.includes('rotation')) trackRotation(entry, next);
|
|
1013
|
+
if (fields.includes('behaviour')) trackBehaviour(entry, next);
|
|
1014
|
+
if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
|
|
1015
|
+
if (fields.includes('text')) entry.hosted?.update(next);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (entry._tag === 'Group' && next._tag === 'Group') {
|
|
1019
|
+
entry.node = next;
|
|
1020
|
+
if (movesTransform(entry, fields)) retarget(entry, next);
|
|
1021
|
+
if (fields.includes('rotation')) trackRotation(entry, next);
|
|
1022
|
+
if (fields.includes('behaviour')) trackBehaviour(entry, next);
|
|
1023
|
+
if (fields.includes('interactive')) trackInteractive(entry.object, next.interactive);
|
|
1024
|
+
if (fields.includes('follow')) {
|
|
1025
|
+
// Back to its own transform, or hidden until the new space is found.
|
|
1026
|
+
entry.object.visible = next.follow === null;
|
|
1027
|
+
if (next.follow === null) {
|
|
1028
|
+
writeVec3(entry.object.position, next.position);
|
|
1029
|
+
trackRotation(entry, next);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (entry._tag === 'AmbientLight' && next._tag === 'AmbientLight') {
|
|
1035
|
+
entry.object.color.setHex(next.color);
|
|
1036
|
+
entry.object.intensity = next.intensity;
|
|
1037
|
+
entry.node = next;
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (entry._tag === 'DirectionalLight' && next._tag === 'DirectionalLight') {
|
|
1041
|
+
writeDirectionalLight(entry.object, next);
|
|
1042
|
+
entry.node = next;
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (entry._tag === 'PointLight' && next._tag === 'PointLight') {
|
|
1046
|
+
writePointLight(entry.object, next);
|
|
1047
|
+
entry.node = next;
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (entry._tag === 'SpotLight' && next._tag === 'SpotLight') {
|
|
1051
|
+
writeSpotLight(entry.object, next);
|
|
1052
|
+
entry.node = next;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (entry._tag === 'HemisphereLight' && next._tag === 'HemisphereLight') {
|
|
1056
|
+
writeHemisphereLight(entry.object, next);
|
|
1057
|
+
entry.node = next;
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (entry._tag === 'ShaderGrid' && next._tag === 'ShaderGrid') {
|
|
1061
|
+
entry.hosted?.update(entry.node, next);
|
|
1062
|
+
entry.node = next;
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (entry._tag === 'Grid' && next._tag === 'Grid') {
|
|
1066
|
+
writeVec3(entry.object.position, next.position);
|
|
1067
|
+
if (fields.some((field) => field !== 'position')) {
|
|
1068
|
+
disposeGridLines(entry.helper);
|
|
1069
|
+
entry.helper = gridLines(next);
|
|
1070
|
+
entry.object.add(entry.helper);
|
|
1071
|
+
}
|
|
1072
|
+
entry.node = next;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
throw new Error(`effect-three: update kind mismatch for "${next.key}"`);
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const parentObject = (parent: ParentKey): Object3D => {
|
|
1079
|
+
if (parent === null) return scene;
|
|
1080
|
+
const entry = entries.get(parent);
|
|
1081
|
+
if (entry === undefined) throw new Error(`effect-three: missing parent "${parent}"`);
|
|
1082
|
+
return entry.object;
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
const register = (key: string, entry: Entry): void => {
|
|
1086
|
+
entries.set(key, entry);
|
|
1087
|
+
entry.object.name = key;
|
|
1088
|
+
keys.set(entry.object, key);
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
const apply = (op: Op): void => {
|
|
1092
|
+
switch (op._tag) {
|
|
1093
|
+
case 'SetCamera': {
|
|
1094
|
+
const previous = camera;
|
|
1095
|
+
camera = cameraFor(camera, op.camera);
|
|
1096
|
+
applyLens(camera, op.camera);
|
|
1097
|
+
// A new kind of camera has nothing on screen to travel from.
|
|
1098
|
+
cameraMotion.aim(camera, op.camera, elapsed, camera !== previous || !animating);
|
|
1099
|
+
fitCamera(camera, viewport);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
case 'SetRenderSettings':
|
|
1103
|
+
settings = op.settings;
|
|
1104
|
+
return;
|
|
1105
|
+
case 'SetViews':
|
|
1106
|
+
hostViews(op.views);
|
|
1107
|
+
return;
|
|
1108
|
+
case 'DefineResources':
|
|
1109
|
+
store.define(op.resources);
|
|
1110
|
+
return;
|
|
1111
|
+
case 'Create': {
|
|
1112
|
+
const entry = build(op.node);
|
|
1113
|
+
register(op.node.key, entry);
|
|
1114
|
+
parentObject(op.parent).add(entry.object);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
case 'Replace': {
|
|
1118
|
+
const old = entries.get(op.node.key);
|
|
1119
|
+
if (old === undefined) throw new Error(`effect-three: missing node "${op.node.key}"`);
|
|
1120
|
+
const entry = build(op.node);
|
|
1121
|
+
const target = parentObject(op.parent);
|
|
1122
|
+
// Children follow their parent key. Later Move/Remove ops settle any that leave.
|
|
1123
|
+
// Copy first: add() removes each child from the array being iterated.
|
|
1124
|
+
for (const child of old.object.children.slice()) entry.object.add(child);
|
|
1125
|
+
const position = old.object.parent === target ? target.children.indexOf(old.object) : -1;
|
|
1126
|
+
old.object.removeFromParent();
|
|
1127
|
+
target.add(entry.object);
|
|
1128
|
+
if (position >= 0) {
|
|
1129
|
+
target.children.pop();
|
|
1130
|
+
target.children.splice(position, 0, entry.object);
|
|
1131
|
+
}
|
|
1132
|
+
release(old);
|
|
1133
|
+
keys.delete(old.object);
|
|
1134
|
+
register(op.node.key, entry);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
case 'Move': {
|
|
1138
|
+
const entry = entries.get(op.key);
|
|
1139
|
+
if (entry === undefined) throw new Error(`effect-three: missing node "${op.key}"`);
|
|
1140
|
+
const target = parentObject(op.parent);
|
|
1141
|
+
if (isPlaced(entry)) {
|
|
1142
|
+
// Keep the world position across the move; settle() then takes the node to its
|
|
1143
|
+
// new local position, instantly or along its transition.
|
|
1144
|
+
entry.object.getWorldPosition(worldPosition);
|
|
1145
|
+
target.add(entry.object);
|
|
1146
|
+
target.updateWorldMatrix(true, false);
|
|
1147
|
+
entry.object.position.copy(target.worldToLocal(worldPosition));
|
|
1148
|
+
reparented.add(entry);
|
|
1149
|
+
} else {
|
|
1150
|
+
target.add(entry.object);
|
|
1151
|
+
}
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
case 'Update': {
|
|
1155
|
+
const entry = entries.get(op.node.key);
|
|
1156
|
+
if (entry === undefined) throw new Error(`effect-three: missing node "${op.node.key}"`);
|
|
1157
|
+
update(entry, op.node, op.fields);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
case 'Remove': {
|
|
1161
|
+
const entry = entries.get(op.key);
|
|
1162
|
+
if (entry === undefined) return;
|
|
1163
|
+
entry.object.removeFromParent();
|
|
1164
|
+
release(entry);
|
|
1165
|
+
entries.delete(op.key);
|
|
1166
|
+
keys.delete(entry.object);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
case 'Order': {
|
|
1170
|
+
const parent = parentObject(op.parent);
|
|
1171
|
+
const ordered: Array<Object3D> = [];
|
|
1172
|
+
for (const key of op.keys) {
|
|
1173
|
+
const entry = entries.get(key);
|
|
1174
|
+
if (entry !== undefined) ordered.push(entry.object);
|
|
1175
|
+
}
|
|
1176
|
+
const managed = new Set(ordered);
|
|
1177
|
+
const unmanaged = parent.children.filter((child) => !managed.has(child));
|
|
1178
|
+
parent.children.splice(0, parent.children.length, ...ordered, ...unmanaged);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
/** The scene a render texture draws: its camera and nodes, with the parent's resources. */
|
|
1185
|
+
const textureScene = (texture: RenderTexture, parent: SceneDescription): SceneDescription => ({
|
|
1186
|
+
camera: texture.camera,
|
|
1187
|
+
gestures: 'page',
|
|
1188
|
+
views: [],
|
|
1189
|
+
renderTextures: [],
|
|
1190
|
+
overlays: [],
|
|
1191
|
+
environment: null,
|
|
1192
|
+
sky: null,
|
|
1193
|
+
contactShadows: null,
|
|
1194
|
+
physics: null,
|
|
1195
|
+
xr: null,
|
|
1196
|
+
measure: [],
|
|
1197
|
+
render: parent.render,
|
|
1198
|
+
resources: parent.resources,
|
|
1199
|
+
children: texture.children,
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
/** A hosted render texture as the loop sees it; none if it is not hosted. */
|
|
1203
|
+
function hostedTexture(texture: RenderTexture): ReadonlyArray<HostedTexture> {
|
|
1204
|
+
const hosted = textureGraphs.get(texture.id);
|
|
1205
|
+
if (hosted === undefined) return [];
|
|
1206
|
+
return [{ id: texture.id, graph: hosted.graph, width: texture.width, height: texture.height }];
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Brings the hosted render textures in line with the description: nested graphs on the
|
|
1211
|
+
* same store, created, reconciled or disposed. Returns their operations.
|
|
1212
|
+
*/
|
|
1213
|
+
const syncTextures = (next: SceneDescription): ReadonlyArray<Op> => {
|
|
1214
|
+
const ops: Array<Op> = [];
|
|
1215
|
+
const kept = new Set<string>();
|
|
1216
|
+
for (const texture of next.renderTextures) {
|
|
1217
|
+
kept.add(texture.id);
|
|
1218
|
+
const existing = textureGraphs.get(texture.id);
|
|
1219
|
+
const hosted = existing ?? {
|
|
1220
|
+
graph: createSceneGraph({
|
|
1221
|
+
store,
|
|
1222
|
+
...(options.behaviours === undefined ? {} : { behaviours: options.behaviours }),
|
|
1223
|
+
}),
|
|
1224
|
+
description: texture,
|
|
1225
|
+
};
|
|
1226
|
+
const result = hosted.graph.reconcile(textureScene(texture, next));
|
|
1227
|
+
const reframed =
|
|
1228
|
+
existing === undefined ||
|
|
1229
|
+
existing.description.width !== texture.width ||
|
|
1230
|
+
existing.description.height !== texture.height ||
|
|
1231
|
+
existing.description.background !== texture.background;
|
|
1232
|
+
if (reframed) {
|
|
1233
|
+
hosted.graph.resize(texture.width, texture.height);
|
|
1234
|
+
hosted.graph.scene.background =
|
|
1235
|
+
texture.background === null ? null : new Color(texture.background);
|
|
1236
|
+
}
|
|
1237
|
+
hosted.description = texture;
|
|
1238
|
+
textureGraphs.set(texture.id, hosted);
|
|
1239
|
+
const subOps = result._tag === 'Applied' ? result.ops : [];
|
|
1240
|
+
if (reframed || subOps.length > 0) {
|
|
1241
|
+
ops.push({ _tag: 'UpdateRenderTexture', id: texture.id, ops: subOps });
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
for (const [id, hosted] of textureGraphs) {
|
|
1245
|
+
if (kept.has(id)) continue;
|
|
1246
|
+
hosted.graph.dispose();
|
|
1247
|
+
textureGraphs.delete(id);
|
|
1248
|
+
ops.push({ _tag: 'RemoveRenderTexture', id });
|
|
1249
|
+
}
|
|
1250
|
+
// In the description's order, so a host draws them in the order they were written.
|
|
1251
|
+
// In the description's order, so a host draws them in the order they were written.
|
|
1252
|
+
textureList = next.renderTextures.flatMap((texture) => hostedTexture(texture));
|
|
1253
|
+
return ops;
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
const reconcile = (next: SceneDescription): ReconcileResult => {
|
|
1257
|
+
// Validate every render texture's scene first, so a bad one changes nothing at all.
|
|
1258
|
+
for (const texture of next.renderTextures) {
|
|
1259
|
+
const checked = indexScene(textureScene(texture, next), knownBehaviours);
|
|
1260
|
+
if (checked._tag === 'Invalid') return checked;
|
|
1261
|
+
}
|
|
1262
|
+
const result = diff(index, next, knownBehaviours);
|
|
1263
|
+
if (result._tag === 'Invalid') return result;
|
|
1264
|
+
for (const op of result.ops) apply(op);
|
|
1265
|
+
for (const entry of reparented) {
|
|
1266
|
+
if (isPlaced(entry)) retarget(entry, entry.node);
|
|
1267
|
+
}
|
|
1268
|
+
reparented.clear();
|
|
1269
|
+
index = result.index;
|
|
1270
|
+
// Gestures change how input is read, not what is drawn: no operation, no frame.
|
|
1271
|
+
gestures = next.gestures;
|
|
1272
|
+
// Overlays are placed when a frame is drawn: a change needs one, but no operation.
|
|
1273
|
+
if (!overlaysEqual(overlayList, next.overlays)) invalidate();
|
|
1274
|
+
overlayList = next.overlays;
|
|
1275
|
+
// The frame loop applies the environment, which may need the renderer: ask for a frame.
|
|
1276
|
+
if (!environmentEquals(environmentDescription, next.environment)) invalidate();
|
|
1277
|
+
environmentDescription = next.environment;
|
|
1278
|
+
if (!skyEquals(skyDescription, next.sky)) {
|
|
1279
|
+
applySky(next.sky);
|
|
1280
|
+
invalidate();
|
|
1281
|
+
}
|
|
1282
|
+
skyDescription = next.sky;
|
|
1283
|
+
// The frame loop draws contact shadows with the renderer: a change needs a frame.
|
|
1284
|
+
if (!contactShadowsEquals(contactDescription, next.contactShadows)) invalidate();
|
|
1285
|
+
contactDescription = next.contactShadows;
|
|
1286
|
+
// The debug lines appear or go with the next frame: ask for one, for an asleep world.
|
|
1287
|
+
if ((physicsDescription?.debug ?? false) !== (next.physics?.debug ?? false)) invalidate();
|
|
1288
|
+
physicsDescription = next.physics;
|
|
1289
|
+
bodyHost.sync(next.physics, bodySources());
|
|
1290
|
+
// Read by the host when it presents the scene immersively: no operation, no frame.
|
|
1291
|
+
xrDescription = next.xr;
|
|
1292
|
+
// Measured after the next drawn frame; a new list needs one.
|
|
1293
|
+
if (next.measure.join('\u0000') !== measured.join('\u0000')) invalidate();
|
|
1294
|
+
measured = next.measure;
|
|
1295
|
+
const ops = [...result.ops, ...syncTextures(next)];
|
|
1296
|
+
if (ops.length > 0) invalidate();
|
|
1297
|
+
return { _tag: 'Applied', ops };
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
// Defined once so ticking allocates nothing; deleting during Map.forEach is safe.
|
|
1301
|
+
const stepTween = (tween: Tween, entry: Entry): void => {
|
|
1302
|
+
if (applyTween(entry.object, tween, elapsed)) moving.delete(entry);
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
const tick = (elapsedSeconds: number): void => {
|
|
1306
|
+
elapsed = elapsedSeconds;
|
|
1307
|
+
for (const texture of textureList) texture.graph.tick(elapsedSeconds);
|
|
1308
|
+
for (const entry of spinning) {
|
|
1309
|
+
if (isPlaced(entry)) {
|
|
1310
|
+
writeRotation(entry.object.rotation, entry.node.rotation, elapsed);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
moving.forEach(stepTween);
|
|
1314
|
+
for (const entry of playing) pose(entry);
|
|
1315
|
+
behaving.forEach(applyBehaviour);
|
|
1316
|
+
cameraMotion.step(camera, elapsed);
|
|
1317
|
+
for (const view of views) viewMotions.get(view.key)?.step(view.camera, elapsed);
|
|
1318
|
+
// Last: a dynamic body's pose is the simulation's, whatever else moved the mesh.
|
|
1319
|
+
bodyHost.tick(elapsed, animating);
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
const finishTween = (tween: Tween, entry: Entry): void => {
|
|
1323
|
+
applyTween(entry.object, tween, Number.POSITIVE_INFINITY);
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
const setAnimating = (next: boolean): void => {
|
|
1327
|
+
if (next !== animating) invalidate();
|
|
1328
|
+
for (const texture of textureList) texture.graph.setAnimating(next);
|
|
1329
|
+
animating = next;
|
|
1330
|
+
if (next) return;
|
|
1331
|
+
moving.forEach(finishTween);
|
|
1332
|
+
moving.clear();
|
|
1333
|
+
cameraMotion.settle(camera);
|
|
1334
|
+
for (const view of views) viewMotions.get(view.key)?.settle(view.camera);
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
const dispose = (): void => {
|
|
1338
|
+
removeSky();
|
|
1339
|
+
bodyHost.dispose();
|
|
1340
|
+
for (const texture of textureList) texture.graph.dispose();
|
|
1341
|
+
textureGraphs.clear();
|
|
1342
|
+
textureList = [];
|
|
1343
|
+
for (const entry of entries.values()) {
|
|
1344
|
+
entry.object.removeFromParent();
|
|
1345
|
+
release(entry);
|
|
1346
|
+
}
|
|
1347
|
+
entries.clear();
|
|
1348
|
+
index = emptyIndex;
|
|
1349
|
+
unsubscribeGeometry();
|
|
1350
|
+
unsubscribeMaterial();
|
|
1351
|
+
unsubscribeShared();
|
|
1352
|
+
unsubscribeLoans();
|
|
1353
|
+
invalidationListeners.clear();
|
|
1354
|
+
if (ownsStore) store.dispose();
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
return {
|
|
1358
|
+
scene,
|
|
1359
|
+
kit: options.kit,
|
|
1360
|
+
get camera() {
|
|
1361
|
+
return camera;
|
|
1362
|
+
},
|
|
1363
|
+
views: () => views,
|
|
1364
|
+
holdCameras: (held) => {
|
|
1365
|
+
cameraMotion.hold(held);
|
|
1366
|
+
for (const motion of viewMotions.values()) motion.hold(held);
|
|
1367
|
+
},
|
|
1368
|
+
turned: (view) => {
|
|
1369
|
+
if (view === undefined) return cameraMotion.turned();
|
|
1370
|
+
return viewMotions.get(view)?.turned();
|
|
1371
|
+
},
|
|
1372
|
+
renderTextures: () => textureList,
|
|
1373
|
+
gestures: () => gestures,
|
|
1374
|
+
overlays: () => overlayList,
|
|
1375
|
+
environment: () => environmentDescription,
|
|
1376
|
+
contactShadows: () => contactDescription,
|
|
1377
|
+
collisions: bodyHost.drainCollisions,
|
|
1378
|
+
contactForces: bodyHost.drainContactForces,
|
|
1379
|
+
xr: () => xrDescription,
|
|
1380
|
+
measured: () => measured,
|
|
1381
|
+
labelLayout: (key) => {
|
|
1382
|
+
const entry = entries.get(key);
|
|
1383
|
+
return entry?._tag === 'Label' ? entry.hosted?.layout() : undefined;
|
|
1384
|
+
},
|
|
1385
|
+
follow: (locate) => {
|
|
1386
|
+
scene.updateMatrixWorld();
|
|
1387
|
+
for (const entry of entries.values()) {
|
|
1388
|
+
if (entry._tag !== 'Group' || entry.node.follow === null) continue;
|
|
1389
|
+
const pose = locate(entry.node.follow);
|
|
1390
|
+
entry.object.visible = pose !== undefined;
|
|
1391
|
+
if (pose !== undefined) placeObject(entry.object, pose);
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
store,
|
|
1395
|
+
reconcile,
|
|
1396
|
+
tick,
|
|
1397
|
+
elapsedSeconds: () => elapsed,
|
|
1398
|
+
renderSettings: () => settings,
|
|
1399
|
+
needsFrame: () =>
|
|
1400
|
+
dirty ||
|
|
1401
|
+
textureList.some((texture) => texture.graph.needsFrame()) ||
|
|
1402
|
+
(animating &&
|
|
1403
|
+
(spinning.size > 0 ||
|
|
1404
|
+
moving.size > 0 ||
|
|
1405
|
+
behaving.size > 0 ||
|
|
1406
|
+
clipsRunning() ||
|
|
1407
|
+
cameraMotion.moving() ||
|
|
1408
|
+
viewsMoving() ||
|
|
1409
|
+
bodyHost.needsFrame())),
|
|
1410
|
+
drawn: () => {
|
|
1411
|
+
dirty = false;
|
|
1412
|
+
},
|
|
1413
|
+
onInvalidated: (listener) => {
|
|
1414
|
+
invalidationListeners.add(listener);
|
|
1415
|
+
return () => {
|
|
1416
|
+
invalidationListeners.delete(listener);
|
|
1417
|
+
};
|
|
1418
|
+
},
|
|
1419
|
+
setAnimating,
|
|
1420
|
+
object: (key) => entries.get(key)?.object,
|
|
1421
|
+
keyOf: (object) => keys.get(object),
|
|
1422
|
+
instanceKeyOf: (object, instanceId) => {
|
|
1423
|
+
const key = keys.get(object);
|
|
1424
|
+
const entry = key === undefined ? undefined : entries.get(key);
|
|
1425
|
+
return entry?._tag === 'Instances' ? entry.node.instances[instanceId]?.key : undefined;
|
|
1426
|
+
},
|
|
1427
|
+
interactiveObjects: () => {
|
|
1428
|
+
interactiveList ??= [...interactive];
|
|
1429
|
+
return interactiveList;
|
|
1430
|
+
},
|
|
1431
|
+
viewport: () => viewport,
|
|
1432
|
+
resize: (width, height) => {
|
|
1433
|
+
viewport = { width, height };
|
|
1434
|
+
fitCamera(camera, viewport);
|
|
1435
|
+
hostViews(viewDescriptions);
|
|
1436
|
+
invalidate();
|
|
1437
|
+
},
|
|
1438
|
+
dispose,
|
|
1439
|
+
};
|
|
1440
|
+
};
|