@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.
Files changed (166) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE +21 -0
  3. package/README.md +191 -2
  4. package/dist/index-4045wme0.js +129 -0
  5. package/dist/index-4045wme0.js.map +10 -0
  6. package/dist/index-ef8pbv9g.js +5118 -0
  7. package/dist/index-ef8pbv9g.js.map +40 -0
  8. package/dist/index.d.ts +12 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +1361 -0
  11. package/dist/index.js.map +14 -0
  12. package/dist/kernel/animation.d.ts +14 -0
  13. package/dist/kernel/animation.d.ts.map +1 -0
  14. package/dist/kernel/behaviours.d.ts +16 -0
  15. package/dist/kernel/behaviours.d.ts.map +1 -0
  16. package/dist/kernel/bodies.d.ts +60 -0
  17. package/dist/kernel/bodies.d.ts.map +1 -0
  18. package/dist/kernel/bodySources.d.ts +47 -0
  19. package/dist/kernel/bodySources.d.ts.map +1 -0
  20. package/dist/kernel/build.d.ts +564 -0
  21. package/dist/kernel/build.d.ts.map +1 -0
  22. package/dist/kernel/cameraMotion.d.ts +39 -0
  23. package/dist/kernel/cameraMotion.d.ts.map +1 -0
  24. package/dist/kernel/clock.d.ts +17 -0
  25. package/dist/kernel/clock.d.ts.map +1 -0
  26. package/dist/kernel/contactShadowPasses.d.ts +47 -0
  27. package/dist/kernel/contactShadowPasses.d.ts.map +1 -0
  28. package/dist/kernel/description.d.ts +1120 -0
  29. package/dist/kernel/description.d.ts.map +1 -0
  30. package/dist/kernel/diff.d.ts +90 -0
  31. package/dist/kernel/diff.d.ts.map +1 -0
  32. package/dist/kernel/environmentPresets.d.ts +30 -0
  33. package/dist/kernel/environmentPresets.d.ts.map +1 -0
  34. package/dist/kernel/equality.d.ts +64 -0
  35. package/dist/kernel/equality.d.ts.map +1 -0
  36. package/dist/kernel/frames.d.ts +55 -0
  37. package/dist/kernel/frames.d.ts.map +1 -0
  38. package/dist/kernel/index.d.ts +37 -0
  39. package/dist/kernel/index.d.ts.map +1 -0
  40. package/dist/kernel/index.js +335 -0
  41. package/dist/kernel/index.js.map +9 -0
  42. package/dist/kernel/instances.d.ts +26 -0
  43. package/dist/kernel/instances.d.ts.map +1 -0
  44. package/dist/kernel/labels.d.ts +45 -0
  45. package/dist/kernel/labels.d.ts.map +1 -0
  46. package/dist/kernel/loop.d.ts +49 -0
  47. package/dist/kernel/loop.d.ts.map +1 -0
  48. package/dist/kernel/materials.d.ts +24 -0
  49. package/dist/kernel/materials.d.ts.map +1 -0
  50. package/dist/kernel/motion.d.ts +36 -0
  51. package/dist/kernel/motion.d.ts.map +1 -0
  52. package/dist/kernel/orbit.d.ts +127 -0
  53. package/dist/kernel/orbit.d.ts.map +1 -0
  54. package/dist/kernel/overlays.d.ts +16 -0
  55. package/dist/kernel/overlays.d.ts.map +1 -0
  56. package/dist/kernel/performance.d.ts +41 -0
  57. package/dist/kernel/performance.d.ts.map +1 -0
  58. package/dist/kernel/physics.d.ts +166 -0
  59. package/dist/kernel/physics.d.ts.map +1 -0
  60. package/dist/kernel/physicsDebug.d.ts +18 -0
  61. package/dist/kernel/physicsDebug.d.ts.map +1 -0
  62. package/dist/kernel/pointer.d.ts +357 -0
  63. package/dist/kernel/pointer.d.ts.map +1 -0
  64. package/dist/kernel/renderKit.d.ts +48 -0
  65. package/dist/kernel/renderKit.d.ts.map +1 -0
  66. package/dist/kernel/renderer.d.ts +83 -0
  67. package/dist/kernel/renderer.d.ts.map +1 -0
  68. package/dist/kernel/resources.d.ts +112 -0
  69. package/dist/kernel/resources.d.ts.map +1 -0
  70. package/dist/kernel/sceneGraph.d.ts +155 -0
  71. package/dist/kernel/sceneGraph.d.ts.map +1 -0
  72. package/dist/kernel/sceneIndex.d.ts +90 -0
  73. package/dist/kernel/sceneIndex.d.ts.map +1 -0
  74. package/dist/kernel/stage.d.ts +87 -0
  75. package/dist/kernel/stage.d.ts.map +1 -0
  76. package/dist/kernel/textLayout.d.ts +29 -0
  77. package/dist/kernel/textLayout.d.ts.map +1 -0
  78. package/dist/kernel/tween.d.ts +35 -0
  79. package/dist/kernel/tween.d.ts.map +1 -0
  80. package/dist/kernel/xrInput.d.ts +107 -0
  81. package/dist/kernel/xrInput.d.ts.map +1 -0
  82. package/dist/kernel/xrLocomotion.d.ts +38 -0
  83. package/dist/kernel/xrLocomotion.d.ts.map +1 -0
  84. package/dist/runtime/FrameSource.d.ts +29 -0
  85. package/dist/runtime/FrameSource.d.ts.map +1 -0
  86. package/dist/runtime/ResourceRegistry.d.ts +18 -0
  87. package/dist/runtime/ResourceRegistry.d.ts.map +1 -0
  88. package/dist/runtime/SceneRuntime.d.ts +68 -0
  89. package/dist/runtime/SceneRuntime.d.ts.map +1 -0
  90. package/dist/runtime/SceneSchema.d.ts +2536 -0
  91. package/dist/runtime/SceneSchema.d.ts.map +1 -0
  92. package/dist/runtime/errors.d.ts +19 -0
  93. package/dist/runtime/errors.d.ts.map +1 -0
  94. package/dist/runtime/index.d.ts +11 -0
  95. package/dist/runtime/index.d.ts.map +1 -0
  96. package/dist/webgl/contactShadows.d.ts +5 -0
  97. package/dist/webgl/contactShadows.d.ts.map +1 -0
  98. package/dist/webgl/index.d.ts +11 -0
  99. package/dist/webgl/index.d.ts.map +1 -0
  100. package/dist/webgl/index.js +239 -0
  101. package/dist/webgl/index.js.map +13 -0
  102. package/dist/webgl/shaderGrid.d.ts +27 -0
  103. package/dist/webgl/shaderGrid.d.ts.map +1 -0
  104. package/dist/webgl/sky.d.ts +4 -0
  105. package/dist/webgl/sky.d.ts.map +1 -0
  106. package/dist/webgpu/contactShadows.d.ts +5 -0
  107. package/dist/webgpu/contactShadows.d.ts.map +1 -0
  108. package/dist/webgpu/index.d.ts +10 -0
  109. package/dist/webgpu/index.d.ts.map +1 -0
  110. package/dist/webgpu/index.js +216 -0
  111. package/dist/webgpu/index.js.map +13 -0
  112. package/dist/webgpu/shaderGrid.d.ts +5 -0
  113. package/dist/webgpu/shaderGrid.d.ts.map +1 -0
  114. package/dist/webgpu/sky.d.ts +4 -0
  115. package/dist/webgpu/sky.d.ts.map +1 -0
  116. package/package.json +82 -4
  117. package/src/index.ts +11 -0
  118. package/src/kernel/animation.ts +24 -0
  119. package/src/kernel/behaviours.ts +36 -0
  120. package/src/kernel/bodies.ts +338 -0
  121. package/src/kernel/bodySources.ts +203 -0
  122. package/src/kernel/build.ts +1325 -0
  123. package/src/kernel/cameraMotion.ts +202 -0
  124. package/src/kernel/clock.ts +35 -0
  125. package/src/kernel/contactShadowPasses.ts +207 -0
  126. package/src/kernel/description.ts +1197 -0
  127. package/src/kernel/diff.ts +383 -0
  128. package/src/kernel/environmentPresets.ts +38 -0
  129. package/src/kernel/equality.ts +604 -0
  130. package/src/kernel/frames.ts +124 -0
  131. package/src/kernel/index.ts +274 -0
  132. package/src/kernel/instances.ts +97 -0
  133. package/src/kernel/labels.ts +48 -0
  134. package/src/kernel/loop.ts +248 -0
  135. package/src/kernel/materials.ts +258 -0
  136. package/src/kernel/motion.ts +75 -0
  137. package/src/kernel/orbit.ts +554 -0
  138. package/src/kernel/overlays.ts +130 -0
  139. package/src/kernel/performance.ts +101 -0
  140. package/src/kernel/physics.ts +361 -0
  141. package/src/kernel/physicsDebug.ts +68 -0
  142. package/src/kernel/pointer.ts +785 -0
  143. package/src/kernel/renderKit.ts +53 -0
  144. package/src/kernel/renderer.ts +136 -0
  145. package/src/kernel/resources.ts +504 -0
  146. package/src/kernel/sceneGraph.ts +1440 -0
  147. package/src/kernel/sceneIndex.ts +219 -0
  148. package/src/kernel/stage.ts +157 -0
  149. package/src/kernel/textLayout.ts +102 -0
  150. package/src/kernel/tween.ts +80 -0
  151. package/src/kernel/xrInput.ts +230 -0
  152. package/src/kernel/xrLocomotion.ts +72 -0
  153. package/src/runtime/FrameSource.ts +41 -0
  154. package/src/runtime/ResourceRegistry.ts +24 -0
  155. package/src/runtime/SceneRuntime.ts +229 -0
  156. package/src/runtime/SceneSchema.ts +983 -0
  157. package/src/runtime/errors.ts +18 -0
  158. package/src/runtime/index.ts +10 -0
  159. package/src/webgl/contactShadows.ts +67 -0
  160. package/src/webgl/index.ts +19 -0
  161. package/src/webgl/shaderGrid.ts +196 -0
  162. package/src/webgl/sky.ts +42 -0
  163. package/src/webgpu/contactShadows.ts +83 -0
  164. package/src/webgpu/index.ts +19 -0
  165. package/src/webgpu/shaderGrid.ts +158 -0
  166. package/src/webgpu/sky.ts +35 -0
@@ -0,0 +1,248 @@
1
+ import { Box3, EquirectangularReflectionMapping, type Texture, UVMapping } from 'three';
2
+
3
+ import type { RenderSettings } from './description.ts';
4
+ import { type OverlayPlacement, placeOverlays } from './overlays.ts';
5
+ import { createPerformanceMonitor, type PerformanceOptions } from './performance.ts';
6
+ /**
7
+ * The per-frame step and pointer routing for one scene. Plain synchronous code: the
8
+ * Effect runtime owns its lifetime (ADR-003: Effect owns lifetimes, not frames).
9
+ *
10
+ * @module
11
+ */
12
+ import {
13
+ createPicker,
14
+ createPointerTracker,
15
+ type PointerInput,
16
+ type SceneEvent,
17
+ } from './pointer.ts';
18
+ import type { RenderTarget, SceneRenderer } from './renderer.ts';
19
+ import type { ContactShadowPasses } from './renderKit.ts';
20
+ import type { SceneGraph } from './sceneGraph.ts';
21
+ import { createXrTracker, type XrInput } from './xrInput.ts';
22
+
23
+ export type SceneLoopOptions = {
24
+ readonly graph: SceneGraph;
25
+ readonly renderer: SceneRenderer;
26
+ /** Scene time in seconds. Behaviours are a pure function of it. */
27
+ readonly elapsedSeconds: () => number;
28
+ /** Receives every scene event. Must not throw. */
29
+ readonly emit: (event: SceneEvent) => void;
30
+ /** Wall-clock milliseconds, for the frame-rate monitor. Defaults to `Date.now`. */
31
+ readonly now?: () => number;
32
+ /** Tuning for the frame-rate monitor. */
33
+ readonly performance?: PerformanceOptions;
34
+ /**
35
+ * Receives where the scene's overlays are after each drawn frame, for the host to move
36
+ * their HTML there; once with none after the last overlay goes. Must not throw.
37
+ */
38
+ readonly placeOverlays?: (placements: ReadonlyArray<OverlayPlacement>) => void;
39
+ };
40
+
41
+ export type SceneLoop = {
42
+ /** Advances behaviours, re-tests hover and renders one frame. */
43
+ readonly frame: () => void;
44
+ /**
45
+ * Whether frames should keep coming: always in `always` mode, while the graph needs one
46
+ * in `demand` mode, never in `never` mode (frames then come only from `advance`).
47
+ */
48
+ readonly wantsFrames: () => boolean;
49
+ /** Releases the render textures' targets and withdraws their textures. */
50
+ readonly dispose: () => void;
51
+ /**
52
+ * Routes pointer input to hit testing, emits the resulting events and returns them, so
53
+ * a DOM listener can decide synchronously whether the scene handled its event.
54
+ */
55
+ readonly pointer: (input: PointerInput) => ReadonlyArray<SceneEvent>;
56
+ /** Routes XR input (aims, selects, sticks, buttons, surfaces), emitting the events. */
57
+ readonly xrInput: (input: XrInput) => ReadonlyArray<SceneEvent>;
58
+ };
59
+
60
+ /** Creates the frame step and pointer router for one scene. */
61
+ const noContactShadows: ContactShadowPasses = { draw: () => undefined, dispose: () => undefined };
62
+
63
+ export const createSceneLoop = (options: SceneLoopOptions): SceneLoop => {
64
+ const { graph, renderer, elapsedSeconds, emit } = options;
65
+ const picker = createPicker(graph);
66
+ const tracker = createPointerTracker(picker);
67
+ const xrTracker = createXrTracker(picker);
68
+ const monitor = createPerformanceMonitor(options.performance);
69
+ const now = options.now ?? Date.now;
70
+ let configured: RenderSettings | undefined;
71
+ // Whether overlays were placed last frame, so their going is reported once.
72
+ let placedAny = false;
73
+ // One offscreen target per render texture, its texture lent to the store under its id.
74
+ const targets = new Map<
75
+ string,
76
+ {
77
+ readonly target: RenderTarget;
78
+ readonly width: number;
79
+ readonly height: number;
80
+ readonly withdraw: () => void;
81
+ fresh: boolean;
82
+ }
83
+ >();
84
+
85
+ const release = (id: string): void => {
86
+ const held = targets.get(id);
87
+ if (held === undefined) return;
88
+ held.withdraw();
89
+ held.target.dispose();
90
+ targets.delete(id);
91
+ };
92
+
93
+ /** Draws each render texture whose scene changed or animates, into its own target. */
94
+ const drawTextures = (): void => {
95
+ const textures = graph.renderTextures();
96
+ for (const texture of textures) {
97
+ const held = targets.get(texture.id);
98
+ if (held === undefined || held.width !== texture.width || held.height !== texture.height) {
99
+ release(texture.id);
100
+ const target = renderer.createTarget(texture.width, texture.height);
101
+ const withdraw = graph.store.lendTexture(texture.id, target.texture);
102
+ targets.set(texture.id, {
103
+ target,
104
+ width: texture.width,
105
+ height: texture.height,
106
+ withdraw,
107
+ fresh: true,
108
+ });
109
+ }
110
+ const current = targets.get(texture.id);
111
+ if (current !== undefined && (current.fresh || texture.graph.needsFrame())) {
112
+ renderer.render(texture.graph.scene, texture.graph.camera, undefined, current.target);
113
+ texture.graph.drawn();
114
+ current.fresh = false;
115
+ }
116
+ }
117
+ if (targets.size > textures.length) {
118
+ const live = new Set(textures.map((texture) => texture.id));
119
+ for (const id of targets.keys()) if (!live.has(id)) release(id);
120
+ }
121
+ };
122
+ // Contact shadows are shaded: the render kit draws them, or nothing does.
123
+ const contact = graph.kit?.contactShadows(graph, renderer) ?? noContactShadows;
124
+ // The room environment, made by the renderer while a scene asks for it.
125
+ let room: RenderTarget | undefined;
126
+
127
+ const releaseRoom = (): void => {
128
+ room?.dispose();
129
+ room = undefined;
130
+ };
131
+
132
+ /** The environment's texture: the room, or the lent map (set to reflect as a panorama). */
133
+ const environmentTexture = (map: string | null): Texture | null => {
134
+ if (map === null) {
135
+ room ??= renderer.createEnvironment();
136
+ return room.texture;
137
+ }
138
+ releaseRoom();
139
+ const texture = graph.store.texture(map) ?? null;
140
+ // As Drei's useEnvironment: an equirectangular image maps as a reflection panorama.
141
+ if (texture !== null && texture.mapping === UVMapping) {
142
+ texture.mapping = EquirectangularReflectionMapping;
143
+ texture.needsUpdate = true;
144
+ }
145
+ return texture;
146
+ };
147
+
148
+ /** Applies the scene's environment as Drei's `setEnvProps` does. */
149
+ const applyEnvironment = (): void => {
150
+ const environment = graph.environment();
151
+ const { scene } = graph;
152
+ if (environment === null) {
153
+ releaseRoom();
154
+ scene.environment = null;
155
+ scene.background = null;
156
+ return;
157
+ }
158
+ const texture = environmentTexture(environment.map);
159
+ scene.environment = environment.background === 'only' ? null : texture;
160
+ scene.background = environment.background === false ? null : texture;
161
+ scene.backgroundBlurriness = environment.backgroundBlurriness;
162
+ scene.backgroundIntensity = environment.backgroundIntensity;
163
+ scene.environmentIntensity = environment.environmentIntensity;
164
+ };
165
+
166
+ // The last box reported for each measured key, so only changes are reported.
167
+ const reported = new Map<string, Box3>();
168
+ const box = new Box3();
169
+
170
+ // The last layout reported for each measured label.
171
+ const laidOut = new Map<string, string>();
172
+
173
+ const measureLabel = (key: string): void => {
174
+ const layout = graph.labelLayout(key);
175
+ if (layout === undefined) return;
176
+ const signature = [...layout.block, ...layout.visible].join(',');
177
+ if (laidOut.get(key) === signature) return;
178
+ laidOut.set(key, signature);
179
+ emit({ _tag: 'LabelMeasured', key, block: layout.block, visible: layout.visible });
180
+ };
181
+
182
+ const measure = (): void => {
183
+ const keys = graph.measured();
184
+ if (keys.length === 0 && reported.size === 0 && laidOut.size === 0) return;
185
+ for (const key of keys) {
186
+ measureLabel(key);
187
+ const object = graph.object(key);
188
+ if (object === undefined) continue;
189
+ box.setFromObject(object);
190
+ if (box.isEmpty()) continue;
191
+ const last = reported.get(key);
192
+ if (last !== undefined && last.equals(box)) continue;
193
+ reported.set(key, box.clone());
194
+ emit({ _tag: 'Measured', key, min: box.min.toArray(), max: box.max.toArray() });
195
+ }
196
+ for (const key of reported.keys()) if (!keys.includes(key)) reported.delete(key);
197
+ for (const key of laidOut.keys()) if (!keys.includes(key)) laidOut.delete(key);
198
+ };
199
+
200
+ return {
201
+ frame: () => {
202
+ graph.tick(elapsedSeconds());
203
+ for (const event of tracker.frame()) emit(event);
204
+ for (const collision of graph.collisions()) emit({ _tag: 'Collision', ...collision });
205
+ for (const force of graph.contactForces()) emit({ _tag: 'ContactForce', ...force });
206
+ // Settings change only through reconcile, which replaces the value: compare by reference.
207
+ const settings = graph.renderSettings();
208
+ if (settings !== configured) {
209
+ renderer.configure(settings);
210
+ configured = settings;
211
+ }
212
+ drawTextures();
213
+ applyEnvironment();
214
+ contact.draw();
215
+ const views = graph.views();
216
+ if (views.length === 0) renderer.render(graph.scene, graph.camera);
217
+ for (const view of views) renderer.render(graph.scene, view.camera, view.area);
218
+ graph.drawn();
219
+ measure();
220
+ const overlays = graph.overlays();
221
+ if (options.placeOverlays !== undefined && (overlays.length > 0 || placedAny)) {
222
+ options.placeOverlays(placeOverlays(graph));
223
+ placedAny = overlays.length > 0;
224
+ }
225
+ const change = monitor.sample(now());
226
+ if (change !== undefined) emit({ _tag: 'PerformanceChanged', ...change });
227
+ },
228
+ dispose: () => {
229
+ for (const id of targets.keys()) release(id);
230
+ releaseRoom();
231
+ contact.dispose();
232
+ },
233
+ wantsFrames: () => {
234
+ const mode = graph.renderSettings().frames;
235
+ return mode === 'always' || (mode === 'demand' && graph.needsFrame());
236
+ },
237
+ pointer: (input) => {
238
+ const events = tracker.input(input);
239
+ for (const event of events) emit(event);
240
+ return events;
241
+ },
242
+ xrInput: (input) => {
243
+ const events = xrTracker(input);
244
+ for (const event of events) emit(event);
245
+ return events;
246
+ },
247
+ };
248
+ };
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Building and updating three.js materials from descriptions.
3
+ *
4
+ * Kinds and defaults follow three.js's mesh materials (`three/src/materials`). A change
5
+ * within one kind is applied in place; when it touches something compiled into the shader
6
+ * (transparency, side, flat shading) the material is flagged for recompilation, as three
7
+ * requires. A change of kind needs a new material, which the resource store handles.
8
+ *
9
+ * @module
10
+ */
11
+ import {
12
+ BackSide,
13
+ DoubleSide,
14
+ FrontSide,
15
+ MeshBasicMaterial,
16
+ MeshLambertMaterial,
17
+ MeshNormalMaterial,
18
+ MeshPhongMaterial,
19
+ MeshPhysicalMaterial,
20
+ MeshStandardMaterial,
21
+ ShadowMaterial as ThreeShadowMaterial,
22
+ type Side,
23
+ type Texture,
24
+ } from 'three';
25
+
26
+ import type {
27
+ BasicMaterial,
28
+ LambertMaterial,
29
+ MaterialSide,
30
+ NormalMaterial,
31
+ PhongMaterial,
32
+ PhysicalMaterial,
33
+ ShadowMaterial,
34
+ StandardMaterial,
35
+ SurfaceMaterial,
36
+ } from './description.ts';
37
+
38
+ /** Looks up a texture lent to the store. */
39
+ export type Textures = (id: string) => Texture | undefined;
40
+
41
+ const noTextures: Textures = () => undefined;
42
+
43
+ /** Sets the colour map. Returns whether the shader must recompile (a map came or went). */
44
+ const applyMap = (
45
+ material: Exclude<HostMaterial, MeshNormalMaterial | ThreeShadowMaterial>,
46
+ id: string | null,
47
+ textures: Textures,
48
+ ): boolean => {
49
+ const map = id === null ? null : (textures(id) ?? null);
50
+ const recompile = (material.map === null) !== (map === null);
51
+ material.map = map;
52
+ return recompile;
53
+ };
54
+
55
+ /** The three.js material kinds the kernel hosts. */
56
+ export type HostMaterial =
57
+ | MeshBasicMaterial
58
+ | MeshLambertMaterial
59
+ | MeshPhongMaterial
60
+ | MeshStandardMaterial
61
+ | MeshPhysicalMaterial
62
+ | MeshNormalMaterial
63
+ | ThreeShadowMaterial;
64
+
65
+ const sides = { front: FrontSide, back: BackSide, double: DoubleSide } satisfies Record<
66
+ MaterialSide,
67
+ Side
68
+ >;
69
+
70
+ type SurfaceFields = {
71
+ readonly opacity: number;
72
+ readonly transparent: boolean;
73
+ readonly side: MaterialSide;
74
+ readonly wireframe: boolean;
75
+ };
76
+
77
+ type Shaded = SurfaceFields & { readonly flatShading: boolean };
78
+
79
+ /** Applies the fields every kind shares. Returns whether the shader must recompile. */
80
+ const applySurface = (
81
+ material: Exclude<HostMaterial, ThreeShadowMaterial>,
82
+ description: SurfaceFields,
83
+ ): boolean => {
84
+ const side = sides[description.side];
85
+ const recompile = material.transparent !== description.transparent || material.side !== side;
86
+ material.opacity = description.opacity;
87
+ material.transparent = description.transparent;
88
+ material.side = side;
89
+ material.wireframe = description.wireframe;
90
+ return recompile;
91
+ };
92
+
93
+ const applyShaded = (
94
+ material: Exclude<HostMaterial, MeshBasicMaterial | ThreeShadowMaterial>,
95
+ description: Shaded,
96
+ mapChanged = false,
97
+ ): void => {
98
+ const flatChanged = material.flatShading !== description.flatShading;
99
+ material.flatShading = description.flatShading;
100
+ if (applySurface(material, description) || flatChanged || mapChanged) {
101
+ material.needsUpdate = true;
102
+ }
103
+ };
104
+
105
+ const applyBasic = (
106
+ material: MeshBasicMaterial,
107
+ description: BasicMaterial,
108
+ textures: Textures,
109
+ ): void => {
110
+ material.color.setHex(description.color);
111
+ const mapChanged = applyMap(material, description.map, textures);
112
+ if (applySurface(material, description) || mapChanged) material.needsUpdate = true;
113
+ };
114
+
115
+ const applyLambert = (
116
+ material: MeshLambertMaterial,
117
+ description: LambertMaterial,
118
+ textures: Textures,
119
+ ): void => {
120
+ material.color.setHex(description.color);
121
+ material.emissive.setHex(description.emissive);
122
+ applyShaded(material, description, applyMap(material, description.map, textures));
123
+ };
124
+
125
+ const applyPhong = (
126
+ material: MeshPhongMaterial,
127
+ description: PhongMaterial,
128
+ textures: Textures,
129
+ ): void => {
130
+ material.color.setHex(description.color);
131
+ material.emissive.setHex(description.emissive);
132
+ material.specular.setHex(description.specular);
133
+ material.shininess = description.shininess;
134
+ applyShaded(material, description, applyMap(material, description.map, textures));
135
+ };
136
+
137
+ const applyStandard = (
138
+ material: MeshStandardMaterial,
139
+ description: StandardMaterial | PhysicalMaterial,
140
+ textures: Textures,
141
+ ): void => {
142
+ material.color.setHex(description.color);
143
+ material.roughness = description.roughness;
144
+ material.metalness = description.metalness;
145
+ material.emissive.setHex(description.emissive);
146
+ material.emissiveIntensity = description.emissiveIntensity;
147
+ applyShaded(material, description, applyMap(material, description.map, textures));
148
+ };
149
+
150
+ const applyPhysical = (
151
+ material: MeshPhysicalMaterial,
152
+ description: PhysicalMaterial,
153
+ textures: Textures,
154
+ ): void => {
155
+ applyStandard(material, description, textures);
156
+ // three's clearcoat and transmission setters recompile when crossing zero.
157
+ material.clearcoat = description.clearcoat;
158
+ material.clearcoatRoughness = description.clearcoatRoughness;
159
+ material.transmission = description.transmission;
160
+ material.ior = description.ior;
161
+ };
162
+
163
+ const applyNormal = (material: MeshNormalMaterial, description: NormalMaterial): void => {
164
+ applyShaded(material, description);
165
+ };
166
+
167
+ const applyShadow = (material: ThreeShadowMaterial, description: ShadowMaterial): void => {
168
+ material.color.setHex(description.color);
169
+ material.opacity = description.opacity;
170
+ };
171
+
172
+ /** Builds a new material for a description. */
173
+ export const buildMaterial = (
174
+ description: SurfaceMaterial,
175
+ textures: Textures = noTextures,
176
+ ): HostMaterial => {
177
+ switch (description._tag) {
178
+ case 'Shadow': {
179
+ const material = new ThreeShadowMaterial();
180
+ applyShadow(material, description);
181
+ return material;
182
+ }
183
+ case 'Basic': {
184
+ const material = new MeshBasicMaterial();
185
+ applyBasic(material, description, textures);
186
+ return material;
187
+ }
188
+ case 'Lambert': {
189
+ const material = new MeshLambertMaterial();
190
+ applyLambert(material, description, textures);
191
+ return material;
192
+ }
193
+ case 'Phong': {
194
+ const material = new MeshPhongMaterial();
195
+ applyPhong(material, description, textures);
196
+ return material;
197
+ }
198
+ case 'Standard': {
199
+ const material = new MeshStandardMaterial();
200
+ applyStandard(material, description, textures);
201
+ return material;
202
+ }
203
+ case 'Physical': {
204
+ const material = new MeshPhysicalMaterial();
205
+ applyPhysical(material, description, textures);
206
+ return material;
207
+ }
208
+ case 'Normal': {
209
+ const material = new MeshNormalMaterial();
210
+ applyNormal(material, description);
211
+ return material;
212
+ }
213
+ }
214
+ };
215
+
216
+ /**
217
+ * Applies a description to an existing material in place. Returns `false`, changing
218
+ * nothing, when the material is a different kind: the caller must build a new one.
219
+ */
220
+ export const updateMaterial = (
221
+ material: HostMaterial,
222
+ description: SurfaceMaterial,
223
+ textures: Textures = noTextures,
224
+ ): boolean => {
225
+ // MeshPhysicalMaterial extends MeshStandardMaterial, so test the exact kind by type.
226
+ switch (description._tag) {
227
+ case 'Shadow':
228
+ if (!(material instanceof ThreeShadowMaterial)) return false;
229
+ applyShadow(material, description);
230
+ return true;
231
+ case 'Basic':
232
+ if (!(material instanceof MeshBasicMaterial)) return false;
233
+ applyBasic(material, description, textures);
234
+ return true;
235
+ case 'Lambert':
236
+ if (!(material instanceof MeshLambertMaterial)) return false;
237
+ applyLambert(material, description, textures);
238
+ return true;
239
+ case 'Phong':
240
+ if (!(material instanceof MeshPhongMaterial)) return false;
241
+ applyPhong(material, description, textures);
242
+ return true;
243
+ case 'Standard':
244
+ if (material.type !== 'MeshStandardMaterial' || !(material instanceof MeshStandardMaterial)) {
245
+ return false;
246
+ }
247
+ applyStandard(material, description, textures);
248
+ return true;
249
+ case 'Physical':
250
+ if (!(material instanceof MeshPhysicalMaterial)) return false;
251
+ applyPhysical(material, description, textures);
252
+ return true;
253
+ case 'Normal':
254
+ if (!(material instanceof MeshNormalMaterial)) return false;
255
+ applyNormal(material, description);
256
+ return true;
257
+ }
258
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Registered behaviours: application motion as pure functions of scene time.
3
+ *
4
+ * A behaviour writes a pose (a position offset, a rotation and a scale factor) into a
5
+ * reused scratch value, so ticking allocates nothing. The scene graph composes that pose
6
+ * on top of the node's own transform each frame: position added, rotation added, scale
7
+ * multiplied. Because the pose depends only on time and parameters, behaviours pause,
8
+ * rewind and replay with the scene, unlike `useFrame` callbacks that mutate state.
9
+ *
10
+ * @module
11
+ */
12
+ import { Vector3 } from 'three';
13
+
14
+ /** Where a behaviour writes its pose. Reset to no change before every call. */
15
+ export type BehaviourPose = {
16
+ /** Added to the node's position, in its parent's space. */
17
+ readonly offset: Vector3;
18
+ /** Euler angles (radians) added to the node's rotation. */
19
+ readonly rotation: Vector3;
20
+ /** Multiplies the node's scale. */
21
+ readonly scale: Vector3;
22
+ };
23
+
24
+ /** A behaviour: writes the pose for `seconds` of scene time and the node's parameters. */
25
+ export type Behaviour = (
26
+ pose: BehaviourPose,
27
+ seconds: number,
28
+ params: Readonly<Record<string, number>>,
29
+ ) => void;
30
+
31
+ /** A pose that changes nothing. */
32
+ export const createPose = (): BehaviourPose => ({
33
+ offset: new Vector3(),
34
+ rotation: new Vector3(),
35
+ scale: new Vector3(1, 1, 1),
36
+ });
37
+
38
+ export const resetPose = (pose: BehaviourPose): void => {
39
+ pose.offset.set(0, 0, 0);
40
+ pose.rotation.set(0, 0, 0);
41
+ pose.scale.set(1, 1, 1);
42
+ };
43
+
44
+ /** Maps `value` from one range to another, as three's `MathUtils.mapLinear`. */
45
+ const mapLinear = (value: number, a1: number, a2: number, b1: number, b2: number): number =>
46
+ b1 + ((value - a1) * (b2 - b1)) / (a2 - a1);
47
+
48
+ /**
49
+ * Drei's `Float` (`drei-main/src/core/Float.tsx` at `bf6f4ad`): a gentle bob and sway.
50
+ * Parameters (Drei's defaults): `speed` 1, `rotationIntensity` 1, `floatIntensity` 1,
51
+ * `low` -0.1 and `high` 0.1 (Drei's `floatingRange`), and `phase` 0 in place of Drei's
52
+ * random start, so the motion is the same on every run.
53
+ */
54
+ export const float: Behaviour = (pose, seconds, params) => {
55
+ const speed = params['speed'] ?? 1;
56
+ const turn = params['rotationIntensity'] ?? 1;
57
+ const lift = params['floatIntensity'] ?? 1;
58
+ const angle = (((params['phase'] ?? 0) + seconds) / 4) * speed;
59
+ pose.rotation.set(
60
+ (Math.cos(angle) / 8) * turn,
61
+ (Math.sin(angle) / 8) * turn,
62
+ (Math.sin(angle) / 20) * turn,
63
+ );
64
+ const y = mapLinear(
65
+ Math.sin(angle) / 10,
66
+ -0.1,
67
+ 0.1,
68
+ params['low'] ?? -0.1,
69
+ params['high'] ?? 0.1,
70
+ );
71
+ pose.offset.set(0, y * lift, 0);
72
+ };
73
+
74
+ /** Behaviours every scene knows without registering them. */
75
+ export const builtinBehaviours: Readonly<Record<string, Behaviour>> = { float };