@threenative/core 0.2.0 → 0.3.1

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/capabilities.json +5292 -0
  4. package/dist/assets-kyoF7JlJ.d.ts +103 -0
  5. package/dist/audio-BFiGneTL.d.ts +156 -0
  6. package/dist/canvas-layer-BLVijiUJ.d.ts +62 -0
  7. package/dist/game-XGrTzapq.d.ts +1166 -0
  8. package/dist/gpu-readback-D2iRvoe9.d.ts +112 -0
  9. package/dist/hot.d.ts +20 -2
  10. package/dist/hot.js +14 -2
  11. package/dist/index.d.ts +2108 -55
  12. package/dist/index.js +15645 -2222
  13. package/dist/net.d.ts +65 -0
  14. package/dist/net.js +643 -0
  15. package/dist/playtest.d.ts +37 -4
  16. package/dist/playtest.js +246 -546
  17. package/dist/react.d.ts +177 -0
  18. package/dist/react.js +635 -0
  19. package/dist/renderer-C6hqZpoG.d.ts +770 -0
  20. package/dist/ui-layer.d.ts +306 -0
  21. package/dist/ui-layer.js +425 -0
  22. package/dist/world.d.ts +254 -0
  23. package/dist/world.js +2686 -0
  24. package/gpl/LICENSE.GPL +117 -0
  25. package/gpl/convert.py +192 -0
  26. package/gpl/recipes/_common.py +169 -0
  27. package/gpl/recipes/bake_ao.py +111 -0
  28. package/gpl/recipes/decimate.py +64 -0
  29. package/gpl/recipes/retarget.py +131 -0
  30. package/gpl/recipes/unwrap.py +71 -0
  31. package/mcp/assets.mjs +5 -0
  32. package/mcp/blender-server.mjs +632 -0
  33. package/mcp/blender.mjs +27 -0
  34. package/mcp/engine-server.mjs +501 -0
  35. package/mcp/engine.mjs +31 -0
  36. package/mcp/install.d.mts +37 -0
  37. package/mcp/install.mjs +145 -0
  38. package/mcp/launch.mjs +72 -0
  39. package/mcp/sculpt.mjs +5 -0
  40. package/mcp/servers.d.mts +34 -0
  41. package/mcp/servers.mjs +160 -0
  42. package/package.json +76 -6
  43. package/patches/three@0.185.1.patch +522 -0
  44. package/scripts/apply-three-patch.mjs +297 -0
  45. package/scripts/ensure-mcp.mjs +43 -0
  46. package/scripts/postinstall.mjs +6 -0
  47. package/dist/audio-CEAw0w5y.d.ts +0 -35
  48. package/dist/game-DRt1Qhq3.d.ts +0 -429
package/dist/index.d.ts CHANGED
@@ -1,70 +1,1452 @@
1
- import { AnimationMixer, AnimationClip, Object3D, Sprite, Vector3, CatmullRomCurve3 } from 'three';
2
- export { A as AudioBus, I as IAudioBusOptions, b as IAudioPlayOptions } from './audio-CEAw0w5y.js';
3
- import { a as IRendererLike, b as IGamePluginRuntime, c as IGamePluginHooks } from './game-DRt1Qhq3.js';
4
- export { C as CanvasLayer, d as ICtx, I as IGame, e as IGameObservationContribution, f as IGameObservationSampleRequest, g as IGamePlatformSource, h as IRandom, i as IRawInputPointer, j as IRaycastOptions, k as IScenePickerOptions, S as Scene, l as SceneFrame, m as ScenePicker, n as ScheduleHandle, o as Scheduler, p as createRandom, q as defineGame } from './game-DRt1Qhq3.js';
5
- import { StorageBufferNode, SpriteNodeMaterial, ComputeNode } from 'three/webgpu';
6
- import { IReplayRecording } from '@threenative/playtest';
1
+ import * as three from 'three';
2
+ import { AnimationMixer, Object3D, AnimationClip, Camera, Vector3, Group, Matrix4, Mesh, BufferGeometry, Material, InstancedMesh, ColorRepresentation, Box3, Scene, DirectionalLight, Sprite, DataTexture, CatmullRomCurve3, Texture } from 'three';
3
+ export { I as IAssetLoader, a as IAssetLoaderOptions, c as createAssetLoader, r as reconcileMirroredClips } from './assets-kyoF7JlJ.js';
4
+ export { A as AudioBus, I as IAudioBusOptions, b as IAudioPlayOptions } from './audio-BFiGneTL.js';
5
+ export { C as CanvasLayer } from './canvas-layer-BLVijiUJ.js';
6
+ import { a as IGamePluginRuntime, b as IGamePluginHooks } from './game-XGrTzapq.js';
7
+ export { A as AfterPhysicsCallback, C as ContextMenuPolicy, c as ICtx, I as IGame, d as IGameObservationContribution, e as IGameObservationSampleRequest, f as IGamePlatformSource, g as IInputAction, h as IInputGamepad, i as IPointerDragHandle, j as IPointerEvent3D, k as IPointerEvents3D, l as IPointerEvents3DOptions, m as IPointerEvents3DPicker, n as IPointerState, o as IRandom, p as IRawInputPointer, q as IRawInputPointerEdge, r as IRawInputState, s as IRaycastOptions, t as IScenePickerOptions, u as IThreeNativeAudioConfig, v as IThreeNativeAudioLoop, w as IThreeNativeAudioOverride, x as IThreeNativeAudioSpectrum, y as IThreeNativeBootSplash, z as IThreeNativeConfig, B as IThreeNativeIconVariants, D as IThreeNativeTexturesConfig, E as ITweenOptions, F as IWarmUpCacheOptions, G as IWarmUpObservation, H as IWarmUpOptions, J as IWarmUpProgress, K as IWarmUpRenderer, L as IWarmUpReport, M as InputBindings, N as InputPlatformSource, P as PointerEvent3DListener, O as PointerEvent3DType, Q as PointerEvents3D, S as Scene, R as SceneFrame, T as ScenePicker, U as ScheduleHandle, V as Scheduler, W as ThreeNativeBackgroundMode, X as ThreeNativeOrientation, Y as ThreeNativeUiRenderer, Z as WarmUpCacheStatus, _ as WarmUpObservationStatus, $ as afterPhysics, a0 as createRandom, a1 as defineGame, a2 as warmUpScene } from './game-XGrTzapq.js';
8
+ import * as three_webgpu from 'three/webgpu';
9
+ import { StorageTexture, ComputeNode, UniformNode, Node, TextureNode, NodeMaterial, StorageBufferNode, StructTypeNode, Data3DTexture, ShadowBaseNode, NodeBuilder, NodeFrame, SpriteNodeMaterial, StorageTextureNode } from 'three/webgpu';
10
+ import { I as IRendererLike } from './renderer-C6hqZpoG.js';
11
+ export { D as DEFAULT_PIPELINE_CENSUS_LIMIT, F as FRAME_BUDGET_MARKER, a as FRAME_BUDGET_PHASES, b as FRAME_HITCH_MARKER, c as FrameBudget, d as FrameBudgetPhase, e as IFrameBudgetOptions, f as IFrameBudgetSummary, g as IFrameBudgetWindow, h as IFramePhaseSample, i as IPipelineCensus, j as IPipelineCensusCounts, k as IPipelineCensusEvent, l as IPipelineCensusOptions, m as IPipelineProvenance, n as IPipelineShaderObservation, o as IRenderChainApplied, p as IRenderChainBudgetWindow, q as IRenderChainDroppedStage, r as IRenderChainOptions, s as IRenderChainRenderer, t as IRenderChainRequest, u as IRenderChainStage, v as IRenderChainStageContext, w as IRenderChainVelocityMeasurement, x as IRenderChainVelocityReport, y as IRenderChainVelocityRequest, z as IRenderChainVelocityResult, A as IVelocityRenderPass, P as PIPELINE_CENSUS_CAPABILITY, B as PIPELINE_CENSUS_VERSION, C as PipelineCensus, E as PipelineCensusMode, G as PipelineCensusStatus, R as RENDER_CHAIN_MARKER, H as RENDER_CHAIN_STAGE_ORDER, J as RENDER_CHAIN_TIERS, K as RenderChain, L as RenderChainSource, M as RenderChainStageId, N as RenderChainStageName, O as RenderChainTier, Q as RenderChainTierRequest, S as RenderChainVelocitySource, V as VELOCITY_OUTPUT_NAME, T as VELOCITY_PREVIOUS_BONE_MATRICES, U as VELOCITY_PREVIOUS_INSTANCE_MATRICES, W as VELOCITY_PREVIOUS_WORLD_MATRIX, X as VelocityTracker, Y as createPipelineCensus, Z as ensureVelocityOutput, _ as prewarm, $ as readRenderChainObservation, a0 as readRenderChainReport, a1 as readVelocityPreviousBoneMatrices, a2 as readVelocityPreviousMatrices, a3 as readVelocityPreviousWorldMatrix, a4 as velocityTexture, a5 as withVelocityContext } from './renderer-C6hqZpoG.js';
12
+ import { I as IComputeDriven$1, a as IGPUReadbackSample } from './gpu-readback-D2iRvoe9.js';
13
+ export { C as ComputeDrivenRegistry, G as GPUReadback, b as IGPUReadbackOptions } from './gpu-readback-D2iRvoe9.js';
14
+ import { Node as Node$1 } from 'three/src/nodes/Nodes.js';
7
15
  import 'zustand/vanilla';
8
16
 
9
17
  interface IAnimationPlayerOptions {
10
18
  readonly clips: readonly AnimationClip[];
11
19
  readonly root: Object3D;
20
+ readonly requiredClips?: readonly string[] | Readonly<Record<string, string>>;
21
+ /**
22
+ * Match a travelling clip's playback rate to the ground the body actually covers.
23
+ *
24
+ * On by default, because a model whose feet do not agree with its motion is the single most
25
+ * common thing wrong with a character in a game built here, and every game solves it the same
26
+ * way. Set `false` to keep the authored rate; the measurement below stays live either way and
27
+ * says that it was overridden. The convention re-times locomotion only: a `"once"` clip — a
28
+ * death, a flinch — always plays at its authored rate.
29
+ */
30
+ readonly strideSync?: boolean;
31
+ /**
32
+ * The object whose travel counts as ground covered. Defaults to `root`.
33
+ *
34
+ * Name the body a game moves when the rig is a child of it, which is the usual shape: the clip
35
+ * writes the model's own root track, so measuring the same object the mixer writes would read
36
+ * the clip's motion back as if it were the body's.
37
+ */
38
+ readonly strideRoot?: Object3D;
39
+ }
40
+ /**
41
+ * What the feet are doing against what the body is doing.
42
+ *
43
+ * Reported whether or not the convention is applied: turning a convention off must not turn its
44
+ * measurement off, or a game that opted out has no way to know what it cost.
45
+ */
46
+ interface IStrideReport {
47
+ /** Metres of ground the current clip carries per clip-second, at rate 1. Zero if it travels none. */
48
+ readonly clipGroundSpeed: number;
49
+ /** Metres per second the root has actually covered, smoothed over the last update. */
50
+ readonly groundSpeed: number;
51
+ /** The playback rate those two imply, clamped to `limits`. */
52
+ readonly rate: number;
53
+ /** True when that rate is being applied to the action. */
54
+ readonly synced: boolean;
55
+ /** True when a rate was measured and deliberately not applied. */
56
+ readonly overridden: boolean;
57
+ /**
58
+ * True when the clip carries no root motion and its stride was read off the feet instead.
59
+ *
60
+ * `synced: false, overridden: false` is what an idle reports, so without this a game whose walk
61
+ * cycle is silently going unmatched cannot tell itself apart from one with nothing to match. An
62
+ * in-place clip that also yields no foot plant reports `inPlace: true` with a zero
63
+ * `clipGroundSpeed`, which names the asset as the thing to fix.
64
+ */
65
+ readonly inPlace: boolean;
12
66
  }
13
67
  interface IAnimationPlayOptions {
14
68
  readonly fade?: number;
69
+ /**
70
+ * `"loop"` (default) repeats; `"once"` plays through and holds the last frame. A `"once"` clip
71
+ * also keeps its authored rate — stride sync re-times locomotion, not events.
72
+ */
73
+ readonly mode?: "loop" | "once";
15
74
  }
16
75
  declare class AnimationPlayer {
17
76
  #private;
18
77
  readonly mixer: AnimationMixer;
78
+ readonly root: Object3D;
19
79
  constructor(options: IAnimationPlayerOptions);
20
80
  get current(): string | undefined;
21
81
  get advancedFrames(): number;
82
+ /** True when a `"once"` clip has reached its end and is holding. */
83
+ get finished(): boolean;
84
+ /**
85
+ * What the feet are doing against what the body is doing, as of the last `update`.
86
+ *
87
+ * Live whether or not the convention is applied. A game that set `strideSync: false` reads
88
+ * `overridden: true` here next to the rate it declined, which is the only way an override can
89
+ * be honest about what it turned off.
90
+ */
91
+ get stride(): IStrideReport;
92
+ /** The clip behind a name, for a game that wants the action or the raw `AnimationClip`. */
93
+ clip(name: string): AnimationClip;
22
94
  play(name: string, options?: IAnimationPlayOptions): void;
23
95
  update(dt: number): void;
24
96
  stop(): void;
25
97
  dispose(): void;
26
98
  }
27
99
 
28
- type ThreeNativeOrientation = "landscape" | "portrait" | "sensor";
29
- interface IThreeNativeConfig {
30
- readonly app?: {
31
- readonly id?: string;
32
- readonly name?: string;
33
- readonly version?: string;
34
- readonly build?: number;
35
- readonly icon?: string;
36
- };
37
- readonly display?: {
38
- readonly orientation?: ThreeNativeOrientation;
39
- readonly fullscreen?: boolean;
40
- readonly keepScreenOn?: boolean;
100
+ type NormaliseAxis = "height" | "longest";
101
+ interface INormaliseToMetresOptions {
102
+ readonly metres: number;
103
+ readonly axis: NormaliseAxis;
104
+ /** Crown bone to use for a skinned height measurement. Defaults to a named/highest bone. */
105
+ readonly top?: Object3D | string;
106
+ }
107
+ /**
108
+ * Scale an asset to a real-world size and return the factor applied.
109
+ *
110
+ * Height for a skinned asset comes from its crown bone, not its bind-pose Box3. Longest-axis
111
+ * normalization remains a geometry measurement because it is used for rigid props and weapons.
112
+ */
113
+ declare function normaliseToMetres(object: Object3D, options: INormaliseToMetresOptions): number;
114
+
115
+ interface ISkeletalMesh3DOptions {
116
+ readonly source: Object3D;
117
+ readonly clips?: readonly AnimationClip[];
118
+ readonly requiredClips?: readonly string[] | Readonly<Record<string, string>>;
119
+ readonly size?: INormaliseToMetresOptions;
120
+ readonly strideRoot?: Object3D;
121
+ readonly strideSync?: boolean;
122
+ }
123
+ declare class SkeletalMesh3D extends AnimationPlayer {
124
+ constructor(options: ISkeletalMesh3DOptions);
125
+ }
126
+
127
+ type BillboardLockAxis = "x" | "y" | "z";
128
+ interface IBillboard3DOptions {
129
+ /** Camera whose view direction or position the object follows. */
130
+ readonly camera: Camera;
131
+ /** Restrict the facing rotation to one world axis, for example `"y"` for a tree. */
132
+ readonly lockAxis?: BillboardLockAxis;
133
+ }
134
+ /**
135
+ * Orient one game-owned object toward a camera without owning its geometry or surface.
136
+ *
137
+ * The object is updated only when its owner calls {@link update}; there is no scene-wide registry
138
+ * or per-frame traversal. Perspective cameras face from the object's world position toward the
139
+ * camera, while orthographic cameras use their parallel view direction. The final world rotation
140
+ * is converted back into the object's local space, so a rotated parent remains correct.
141
+ */
142
+ declare class Billboard3D {
143
+ #private;
144
+ readonly object: Object3D;
145
+ readonly camera: Camera;
146
+ readonly lockAxis: BillboardLockAxis | undefined;
147
+ constructor(object: Object3D, options: IBillboard3DOptions);
148
+ /** Apply the current camera pose to the object and return this helper for fluent setup. */
149
+ update(camera?: Camera): this;
150
+ }
151
+
152
+ type CameraShakeCurve = (phase: number) => number;
153
+ interface ICameraShakeOptions {
154
+ /** Position amplitude in world metres, supplied by the game. */
155
+ readonly amplitude: Vector3;
156
+ /** Rotation amplitude in radians around x/y/z, supplied by the game. */
157
+ readonly rotationAmplitude: Vector3;
158
+ /** Curve cycles per second, supplied by the game. */
159
+ readonly frequency: number;
160
+ /** Exponential envelope decay per second, supplied by the game. */
161
+ readonly decay: number;
162
+ /** Game-authored waveform sampled at `elapsed * frequency * 2π`. */
163
+ readonly curve: CameraShakeCurve;
164
+ }
165
+ interface ICameraShakeOffset {
166
+ readonly position: Vector3;
167
+ readonly rotation: Vector3;
168
+ }
169
+ /**
170
+ * Produce a transient camera offset without owning or mutating a camera.
171
+ *
172
+ * The caller supplies the amplitudes, frequency, decay and waveform. {@link update} evaluates the
173
+ * waveform against the caller's fixed-step delta and returns a reusable position/rotation offset;
174
+ * a template can compose it after its own camera rig and damping.
175
+ */
176
+ declare class CameraShake {
177
+ #private;
178
+ readonly amplitude: Vector3;
179
+ readonly rotationAmplitude: Vector3;
180
+ readonly frequency: number;
181
+ readonly decay: number;
182
+ readonly curve: CameraShakeCurve;
183
+ readonly offset: ICameraShakeOffset;
184
+ constructor(options: ICameraShakeOptions);
185
+ get active(): boolean;
186
+ get elapsed(): number;
187
+ /** Start or restart the authored shake waveform. */
188
+ trigger(): this;
189
+ /** Stop the effect and clear the offset. */
190
+ stop(): this;
191
+ /** Evaluate the current offset and advance by one caller-supplied fixed-step delta. */
192
+ update(dt: number): ICameraShakeOffset;
193
+ }
194
+
195
+ type AtmosphereRgb = readonly [number, number, number];
196
+ type AtmosphereVector = AtmosphereRgb | Readonly<{
197
+ x: number;
198
+ y: number;
199
+ z: number;
200
+ }> | Vector3;
201
+ /** Physical inputs for the atmosphere model. Coefficients use 1/km and radii use km. */
202
+ interface IAtmosphereParameters {
203
+ readonly rayleigh: AtmosphereVector;
204
+ readonly mie: AtmosphereVector;
205
+ readonly ozone: AtmosphereVector;
206
+ readonly planetRadius: number;
207
+ readonly atmosphereRadius: number;
208
+ }
209
+ interface IResolvedAtmosphereParameters {
210
+ readonly rayleigh: Vector3;
211
+ readonly mie: Vector3;
212
+ readonly ozone: Vector3;
213
+ readonly planetRadius: number;
214
+ readonly atmosphereRadius: number;
215
+ }
216
+ type IAtmosphereParameterPatch = Partial<IAtmosphereParameters>;
217
+ interface ISolarPositionInput {
218
+ readonly date?: Date | string;
219
+ readonly dayOfYear?: number;
220
+ readonly timeOfDay?: number;
221
+ readonly latitude: number;
222
+ readonly longitude: number;
223
+ /** UTC offset in hours. Use zero when `date` is already UTC. */
224
+ readonly utcOffset?: number;
225
+ }
226
+ interface ISolarPosition {
227
+ elevation: number;
228
+ azimuth: number;
229
+ }
230
+ /** Validate and clone game-owned coefficients. There is intentionally no Earth fallback.
231
+ * @situation validate atmosphere coefficients before a game creates its sky
232
+ * @constraint provide all three coefficient vectors and both radii; omitted fields are errors
233
+ * @example const parameters = resolveAtmosphereParameters({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
234
+ */
235
+ declare function resolveAtmosphereParameters(options: IAtmosphereParameters): IResolvedAtmosphereParameters;
236
+ /** Apply a partial game-owned atmosphere change while preserving validation.
237
+ * @situation change scattering coefficients and rebake an atmosphere
238
+ * @constraint patches cannot introduce omitted, negative, or non-finite physical values
239
+ * @example atmosphere.setAtmosphere({ rayleigh: [0.008, 0.016, 0.04] });
240
+ */
241
+ declare function updateAtmosphereParameters(current: IResolvedAtmosphereParameters, patch: IAtmosphereParameterPatch): IResolvedAtmosphereParameters;
242
+ /**
243
+ * Return the direct vertical transmittance of the supplied atmosphere.
244
+ *
245
+ * The coefficient fixture follows the Hillaire/Bruneton Earth model: an exponential Rayleigh
246
+ * column, an exponential aerosol column, and the triangular ozone column. This CPU value is a
247
+ * small validation oracle for the same coefficients that the GPU LUT kernels consume.
248
+ * @situation check a supplied atmosphere's direct vertical transmittance
249
+ * @constraint use the returned value as a validation oracle; the rendered path samples the LUT
250
+ * @example const zenith = zenithTransmittance({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
251
+ */
252
+ declare function zenithTransmittance(parameters: IAtmosphereParameters | IResolvedAtmosphereParameters): AtmosphereRgb;
253
+ /** Approximate direct transmittance for a ray leaving the ground in a supplied direction.
254
+ * @situation colour a game-owned sun from atmosphere extinction
255
+ * @constraint pass a non-zero direction; coefficients and radii come from the game
256
+ * @example const transmittance = directionalTransmittance(parameters, sunDirection);
257
+ */
258
+ declare function directionalTransmittance(parameters: IAtmosphereParameters | IResolvedAtmosphereParameters, direction: Vector3): Vector3;
259
+ /** Calculate solar elevation and azimuth from time, latitude, and longitude.
260
+ * @situation move a sun across a real day at a game's latitude and longitude
261
+ * @situation run a day and night cycle over the game's sky
262
+ * @constraint dates are interpreted as UTC unless utcOffset is supplied; no fixed sun direction is assumed
263
+ * @constraint pass a mutable { azimuth, elevation } target to reuse the result object in a steady frame loop
264
+ * @example const sun = solarPosition({ date, latitude: 49.28, longitude: -123.12, utcOffset: -8 });
265
+ */
266
+ declare function solarPosition(input: ISolarPositionInput, target?: ISolarPosition): ISolarPosition;
267
+ declare function solarPosition(date: Date | string, latitude: number, longitude: number, target?: ISolarPosition): ISolarPosition;
268
+ /** Convert solar elevation and azimuth degrees into a normalized Three.js direction.
269
+ * @situation aim a template's sun from solarPosition output
270
+ * @constraint elevation and azimuth must be finite degrees
271
+ * @example const direction = directionFromSolarPosition(sun.elevation, sun.azimuth);
272
+ */
273
+ declare function directionFromSolarPosition(elevation: number, azimuth: number): Vector3;
274
+
275
+ interface IAtmosphereLutResolution {
276
+ readonly width: number;
277
+ readonly height: number;
278
+ }
279
+ interface IAtmosphereLutResolutions {
280
+ readonly transmittance: IAtmosphereLutResolution;
281
+ readonly multiScattering: IAtmosphereLutResolution;
282
+ readonly skyView: IAtmosphereLutResolution;
283
+ }
284
+ /** The reference dimensions; games may provide smaller dimensions when their startup budget says so. */
285
+ declare const ATMOSPHERE_LUT_RESOLUTIONS: IAtmosphereLutResolutions;
286
+ declare const LUT_RESOLUTIONS: IAtmosphereLutResolutions;
287
+ interface IParameterUniforms {
288
+ readonly atmosphereRadius: UniformNode<"float", number>;
289
+ readonly mie: UniformNode<"vec3", three.Vector3>;
290
+ readonly ozone: UniformNode<"vec3", three.Vector3>;
291
+ readonly planetRadius: UniformNode<"float", number>;
292
+ readonly rayleigh: UniformNode<"vec3", three.Vector3>;
293
+ }
294
+ /** Resolve the three LUT dimensions, allowing a game to trade startup cost for resolution.
295
+ * @situation choose atmosphere LUT dimensions for a measured startup budget
296
+ * @constraint every width and height must be a positive integer; the dimensions are not a named fidelity tier
297
+ * @example const resolutions = resolveAtmosphereLutResolutions({ skyView: { width: 128, height: 72 } });
298
+ */
299
+ declare function resolveAtmosphereLutResolutions(resolutions: Partial<IAtmosphereLutResolutions> | undefined): IAtmosphereLutResolutions;
300
+ /** Own the transmittance, multi-scattering, and sky-view compute lookup textures.
301
+ * @situation bake the three atmosphere LUTs once before a game shows its world
302
+ * @constraint supply all physical parameters; this class creates no scene appearance
303
+ * @example const luts = new AtmosphereLuts({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
304
+ */
305
+ declare class AtmosphereLuts {
306
+ #private;
307
+ readonly resolutions: IAtmosphereLutResolutions;
308
+ readonly transmittance: StorageTexture;
309
+ readonly multiScattering: StorageTexture;
310
+ readonly skyView: StorageTexture;
311
+ readonly warmupNodes: readonly ComputeNode[];
312
+ readonly uniforms: IParameterUniforms;
313
+ constructor(parameters: IAtmosphereParametersLike, resolutions?: Partial<IAtmosphereLutResolutions>);
314
+ get hash(): string;
315
+ update(parameters: IAtmosphereParametersLike): void;
316
+ sampleTransmittance(uv: Node<"vec2">): TextureNode;
317
+ sampleSkyView(uv: Node<"vec2">): TextureNode;
318
+ dispose(): void;
319
+ }
320
+ type IAtmosphereParametersLike = IResolvedAtmosphereParameters | IAtmosphereParameters;
321
+
322
+ /** The structural contract consumed by the compute registry from PRD-242. */
323
+ interface IComputeDriven {
324
+ readonly warmupNodes: readonly unknown[];
325
+ attachRenderer(renderer: IRendererLike): void;
326
+ readonly processCadence?: "fixed" | "render";
327
+ process(renderer: IRendererLike): void;
328
+ detach(): void;
329
+ readonly released: boolean;
330
+ }
331
+ interface IAtmosphereOptions extends IAtmosphereParameters {
332
+ readonly resolutions?: Partial<IAtmosphereLutResolutions>;
333
+ }
334
+ interface IAtmosphereScenePass {
335
+ getTextureNode(name?: string): Node<"vec4">;
336
+ }
337
+ type AtmosphereDirection = Vector3 | readonly [number, number, number] | Node<"vec3">;
338
+ /**
339
+ * Own the compute lifetime and expose only parameter-driven atmosphere nodes.
340
+ *
341
+ * The class deliberately creates no mesh, material, or scene light. A template chooses all of
342
+ * those, and the same object remains useful when a game supplies a completely different look.
343
+ * @situation render a sunrise that changes as time and place change
344
+ * @situation add distance haze from the depth of a scene pass
345
+ * @alias bright sky saturated green platforms
346
+ * @constraint supply rayleigh, mie, ozone, planetRadius, and atmosphereRadius; there is no Earth fallback
347
+ * @constraint the game creates the sky object, surface, and sun from the returned nodes
348
+ * @example const atmosphere = new Atmosphere({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
349
+ * ctx.add(atmosphere);
350
+ */
351
+ declare class Atmosphere extends Group implements IComputeDriven {
352
+ #private;
353
+ readonly luts: AtmosphereLuts;
354
+ constructor(options: IAtmosphereOptions);
355
+ get parameters(): IResolvedAtmosphereParameters;
356
+ get warmupNodes(): readonly ComputeNode[];
357
+ get released(): boolean;
358
+ get hash(): string;
359
+ attachRenderer(renderer: IRendererLike): void;
360
+ process(renderer?: IRendererLike | undefined): void;
361
+ detach(): void;
362
+ setAtmosphere(patch: IAtmosphereParameterPatch): this;
363
+ setCoefficients(patch: IAtmosphereParameterPatch): this;
364
+ setSunDirection(elevation: number, azimuth: number): this;
365
+ setSunDirection(direction: Vector3): this;
366
+ setSunDirection(position: Pick<ISolarPosition, "elevation" | "azimuth">): this;
367
+ getSunDirection(target?: Vector3): Vector3;
368
+ /** Return the game-owned sky radiance lookup as a TSL vec3 or a CPU validation sample. */
369
+ radiance(direction: AtmosphereDirection): Node<"vec3"> | Vector3;
370
+ /** Return direct solar transmittance as a TSL vec3 or a CPU validation sample. */
371
+ sunTransmittance(direction: AtmosphereDirection): Node<"vec3"> | Vector3;
372
+ /**
373
+ * Composite scene colour against game-supplied in-scattered radiance using scene-pass depth.
374
+ *
375
+ * The optional radiance node lets the game apply its own exposure or artistic tint. Leaving it
376
+ * out uses the raw unit-illumination LUT value and does not introduce a framework look.
377
+ */
378
+ aerialPerspective(scenePass: IAtmosphereScenePass, depth: Node<"float"> | number, inScatteredRadiance?: Node<"vec3"> | Vector3): Node<"vec4">;
379
+ }
380
+
381
+ /** Calculate solar elevation and azimuth for one UTC date.
382
+ * @situation calculate a sun direction from a timestamp and a game location
383
+ * @constraint dates are interpreted as UTC; use solarPosition for an explicit local offset
384
+ * @example const sun = solarPositionAt(new Date(), 49.28, -123.12);
385
+ */
386
+ declare function solarPositionAt(date: Date | string, latitude: number, longitude: number): ISolarPosition;
387
+
388
+ interface IClothTopologyOptions {
389
+ /** Original geometry vertex indices that never move. Duplicate positions pin together. */
390
+ readonly pinned: readonly number[];
391
+ }
392
+
393
+ /** Packed collision input supplied by an optional dependency such as `@threenative/physics`. */
394
+ interface ISoftBodyCollision {
395
+ readonly capacity: number;
396
+ writeBoxes(target: Float32Array, worldToLocal: Matrix4): number;
397
+ }
398
+ interface ISoftBody3DOptions extends IClothTopologyOptions {
399
+ /** Spring acceleration per metre of stretch. Required; the game owns the cloth response. */
400
+ readonly stiffness: number;
401
+ /** Exponential velocity decay per second. Required; zero disables damping. */
402
+ readonly damping: number;
403
+ /** Local-space acceleration in metres per second squared. */
404
+ readonly gravity: readonly [number, number, number];
405
+ /** Local-space wind acceleration in metres per second squared. */
406
+ readonly wind: readonly [number, number, number];
407
+ /** Existing physics bodies translated by the physics package; omitted when cloth has no world collision. */
408
+ readonly collision?: ISoftBodyCollision;
409
+ /** Framework fixed-step duration. Defaults to the engine convention of 1/60 second. */
410
+ readonly timeStep?: number;
411
+ /** Throttled GPU position readback for gameplay/proof; zero disables it. */
412
+ readonly readbackEveryFrames?: number;
413
+ }
414
+ /**
415
+ * Simulate an ordinary game-authored triangle mesh as cloth on the existing fixed-step GPU lane.
416
+ *
417
+ * The mesh supplies every visible choice. This class welds exporter duplicates, owns spring and
418
+ * position buffers, and replaces only the cloned material's position node. It adds no material,
419
+ * colour, texture, wind, stiffness, damping, or pinning default.
420
+ *
421
+ * @situation make a flag, cape, or curtain move as cloth
422
+ * @situation simulate a deforming surface while keeping one edge pinned
423
+ * @situation simulate cloth sails blowing in the wind
424
+ * @situation make cloth sails billow in wind on a ship
425
+ * @constraint the mesh must use one Three.js node material and contain complete triangles
426
+ * @constraint pinned, stiffness, damping, gravity, and wind are required game-owned inputs
427
+ * @constraint Pixel 8 steady upper bound for the shipped 45-vertex pennant with readback every two frames: whole-starter update p95 4.66 ms, render p95 3.56 ms, and GPU timer 0.05 ms across three 300-frame final-rung windows at 552x248 with 4x MSAA; these whole-scene numbers are not isolated solver cost
428
+ * @override timeStep follows the engine 1/60-second convention unless the game overrides it
429
+ * @override readbackEveryFrames enables an explicitly stale CPU position sample; zero disables it
430
+ * @example const flag = new SoftBody3D(flagMesh, { pinned: topEdge, stiffness: 35, damping: 1.8, gravity: [0, -9.81, 0], wind: [1.5, 0, 0.4] });
431
+ */
432
+ declare class SoftBody3D extends Mesh<BufferGeometry, NodeMaterial> implements IComputeDriven$1 {
433
+ #private;
434
+ readonly processCadence: "fixed";
435
+ readonly warmupNodes: readonly ComputeNode[];
436
+ readonly stiffness: number;
437
+ readonly damping: number;
438
+ readonly timeStep: number;
439
+ readonly gravity: Vector3;
440
+ readonly wind: Vector3;
441
+ readonly uniqueVertexCount: number;
442
+ readonly springCount: number;
443
+ constructor(mesh: Mesh, options: ISoftBody3DOptions);
444
+ get released(): boolean;
445
+ get steps(): number;
446
+ /** Latest asynchronous GPU positions and their age, when readback was requested. */
447
+ get sample(): IGPUReadbackSample | undefined;
448
+ debug(): Record<string, unknown>;
449
+ attachRenderer(renderer: IRendererLike): void;
450
+ process(renderer?: IRendererLike | undefined): void;
451
+ detach(): void;
452
+ }
453
+
454
+ /** Selects the meshes that become part of a GPU trace set. */
455
+ interface IGPUSceneBVHOptions {
456
+ readonly include?: (object: Mesh) => boolean;
457
+ }
458
+ /**
459
+ * A material range expressed in packed index elements, after the BVH leaf reorder.
460
+ *
461
+ * `start` and `count` address the uploaded index buffer, not the source geometry, so a single
462
+ * source material can appear as more than one range once the SAH sort interleaves its triangles.
463
+ */
464
+ interface IGPUSceneBVHMaterialGroup {
465
+ readonly count: number;
466
+ readonly materialIndex: number;
467
+ readonly start: number;
468
+ }
469
+ /** The upstream TSL ray query, exposed without renaming or wrapping it. */
470
+ type GPUSceneBVHTraceFunction = (...args: readonly unknown[]) => unknown;
471
+ declare const bvhIntersectFirstHit: GPUSceneBVHTraceFunction;
472
+ declare const rayStruct: StructTypeNode;
473
+ /**
474
+ * Snapshot selected scene meshes into world-space storage buffers for an upstream TSL BVH query.
475
+ *
476
+ * This class owns packing, residency, and release. It deliberately does not own the ray query or
477
+ * a rendered effect: a game imports the exact upstream `bvhIntersectFirstHit` and `rayStruct`
478
+ * exports through the core entry point and uses these four named nodes in its own `src/render/`
479
+ * kernel. The snapshot is static until `rebuild()`.
480
+ */
481
+ declare class GPUSceneBVH extends Group implements IComputeDriven$1 {
482
+ #private;
483
+ readonly indices: StorageBufferNode<"uvec3">;
484
+ readonly nodes: StorageBufferNode<"struct">;
485
+ readonly normals: StorageBufferNode<"vec3">;
486
+ readonly positions: StorageBufferNode<"vec3">;
487
+ readonly warmupNodes: readonly unknown[];
488
+ constructor(scene: Object3D, options?: IGPUSceneBVHOptions);
489
+ get buildMs(): number;
490
+ get materialGroups(): readonly IGPUSceneBVHMaterialGroup[];
491
+ get objectCount(): number;
492
+ get released(): boolean;
493
+ get triangleCount(): number;
494
+ get vertexCount(): number;
495
+ attachRenderer(renderer: IRendererLike): void;
496
+ process(_renderer: IRendererLike): void;
497
+ /** Repack the selected scene objects and replace the GPU buffers behind the stable node handles. */
498
+ rebuild(): void;
499
+ /** Dispose every storage attribute owned by this snapshot. Safe to call more than once. */
500
+ detach(): void;
501
+ }
502
+
503
+ /** One instance's transform, in the units the geometry was authored in. */
504
+ interface IInstancedPlacement {
505
+ /** World position of the instance's origin, as `[x, y, z]`. */
506
+ readonly position: readonly [number, number, number];
507
+ /** Euler rotation in radians, as `[x, y, z]`. Default none. */
508
+ readonly rotation?: readonly [number, number, number];
509
+ /** Per-axis scale as `[x, y, z]`, or one number for all three. Default 1. */
510
+ readonly scale?: readonly [number, number, number] | number;
511
+ }
512
+ interface IInstancedBatchOptions {
513
+ /** The shape every instance draws, supplied by the game. */
514
+ readonly geometry: BufferGeometry;
515
+ /**
516
+ * The surface every instance draws with, supplied by the game and used by reference — recolour
517
+ * it and the whole batch recolours. Required: collapsing the draws is the engine's job, what
518
+ * the props look like never is.
519
+ */
520
+ readonly material: Material;
521
+ }
522
+ interface IInstancedBatchBuildOptions {
523
+ /** Passed straight to the built mesh. Default `false`, as in Three.js. */
524
+ readonly castShadow?: boolean;
525
+ /** Name on the built mesh, so a capture or a scene dump can tell one batch from another. */
526
+ readonly name?: string;
527
+ /** Added to this object when the batch builds. Omit to take the mesh and place it yourself. */
528
+ readonly parent?: Object3D;
529
+ /** Passed straight to the built mesh. Default `false`, as in Three.js. */
530
+ readonly receiveShadow?: boolean;
531
+ }
532
+ /**
533
+ * Collapses many copies of one shape into a single draw, without knowing the count up front.
534
+ *
535
+ * `new InstancedMesh(geometry, material, count)` needs `count` before anything has been placed, so
536
+ * a procedural builder either walks its own layout twice, over-allocates and fixes `.count`
537
+ * afterwards, or gathers transforms into an array first. This is that array, with the
538
+ * `Object3D`-scratch-and-`updateMatrix` dance and the post-fill bookkeeping — `instanceMatrix`
539
+ * invalidation and a bounding sphere the culler can use — done once instead of at every site.
540
+ *
541
+ * It decides nothing about how the result looks: the shape, the surface and every transform are
542
+ * the game's, and the built mesh is handed back so the game can keep animating instances by index.
543
+ */
544
+ declare class InstancedBatch {
545
+ #private;
546
+ readonly geometry: BufferGeometry;
547
+ readonly material: Material;
548
+ constructor(options: IInstancedBatchOptions);
549
+ /** How many instances have been placed so far. */
550
+ get count(): number;
551
+ /** The built mesh, or `undefined` before {@link build} — never a guess. */
552
+ get mesh(): InstancedMesh | undefined;
553
+ /**
554
+ * Records one instance from a matrix the game composed itself, and returns its instance index.
555
+ *
556
+ * The matrix is copied, so the caller may reuse a single scratch `Matrix4` across every call.
557
+ */
558
+ add(matrix: Matrix4): number;
559
+ /** Records one instance from position, scale and Euler rotation, and returns its instance index. */
560
+ place(placement: IInstancedPlacement): number;
561
+ /**
562
+ * Records one instance stretched between two points, and returns its instance index.
563
+ *
564
+ * Chains, tie rods, railing bars, struts and cables are all "from A to B" rather than "at P with
565
+ * rotation R". Deriving the orientation here is what keeps every caller from hand-computing an
566
+ * Euler angle that goes wrong the moment one endpoint moves.
567
+ */
568
+ span(from: readonly [number, number, number], to: readonly [number, number, number], radius: number): number;
569
+ /**
570
+ * Turns everything placed so far into one `InstancedMesh`, and returns it.
571
+ *
572
+ * Returns `undefined` when nothing was placed. That is deliberate: `new InstancedMesh(g, m, 0)`
573
+ * satisfies every type check and draws nothing, so a builder whose layout produced no instances
574
+ * would look identical to one that worked. `undefined` puts that case in the caller's types.
575
+ */
576
+ build(options?: IInstancedBatchBuildOptions): InstancedMesh | undefined;
577
+ }
578
+
579
+ /**
580
+ * One piece on its way into a merged buffer.
581
+ *
582
+ * A `Mesh` is already one of these — pass the meshes straight in and their own transforms are
583
+ * used. Every value here is the game's: the shape, where it sits, and what colour it is.
584
+ */
585
+ interface IMergePart {
586
+ /** The piece's own colour, written flat across its vertices. Omit it on every part for none. */
587
+ readonly color?: ColorRepresentation;
588
+ /** The shape. It is cloned before anything is done to it, so the game's copy is untouched. */
589
+ readonly geometry: BufferGeometry;
590
+ /** Where the piece sits. A `Mesh` brings its own; identity when there is none. */
591
+ readonly matrix?: Matrix4;
592
+ }
593
+ interface IMergePartsOptions {
594
+ /** Named in the error when the merge is refused. Say what was being built. */
595
+ readonly label: string;
596
+ }
597
+ /**
598
+ * Merge game-authored pieces into one buffer, keeping each piece's own colour.
599
+ *
600
+ * Two things go wrong every time an agent bakes a building, a ship or a character out of
601
+ * primitives, and neither is about how any of it looks. `mergeGeometries` returns `null` on
602
+ * mismatched inputs instead of throwing, and the usual mismatch — one non-indexed extrusion among
603
+ * a hundred indexed primitives — is invisible until the whole scene is missing; and a merged
604
+ * buffer draws with one surface, so per-piece colour is gone unless every piece carries a flat
605
+ * `color` attribute written before the merge. Writing that attribute is mechanical. The colours
606
+ * are entirely the game's, one per part, and changing them changes nothing here.
607
+ */
608
+ declare function mergeParts(parts: Iterable<IMergePart>, options: IMergePartsOptions): BufferGeometry;
609
+
610
+ /**
611
+ * A mesh that draws only the clusters this camera can resolve.
612
+ *
613
+ * The asset pipeline bakes a cluster DAG into the `.glb` (`TN_virtual_geometry`); the loader returns
614
+ * one of these when it finds one, and an ordinary `Mesh` when it does not. Nothing about how the
615
+ * mesh looks lives here: `geometry`, `material` and every appearance parameter are the game's, and
616
+ * swapping the surface at any time swaps what draws.
617
+ *
618
+ * The rule, and it asks a cluster nothing about its neighbours: **draw a cluster when its own error
619
+ * projects to fewer screen pixels than the threshold, and its parent group's does not.** Each side
620
+ * is projected through the sphere of the group it belongs to, which is what keeps the cut watertight
621
+ * as the camera moves — a group's sphere encloses every child's, so the parent's projected error can
622
+ * never fall below a child's.
623
+ */
624
+ /** The baked payload, exactly as `TN_virtual_geometry` stores it. */
625
+ interface IClusterTable {
626
+ /** Per cluster, `[centreX, centreY, centreZ, radius]`. Culling reads it; PRD-283 will. */
627
+ readonly bounds: Float32Array;
628
+ /** Per cluster, `[axisX, axisY, axisZ, cutoff]`. */
629
+ readonly cones: Float32Array;
630
+ /** Per cluster, `[ownError, parentError]`, in the mesh's own units. */
631
+ readonly errors: Float32Array;
632
+ /** Cluster-ordered triangles for every level, indexing the geometry's vertex buffer. */
633
+ readonly indices: Uint32Array;
634
+ /** Per cluster, the sphere `parentError` is projected through. */
635
+ readonly parentSpheres: Float32Array;
636
+ /** Per cluster, `[start, count]` into {@link IClusterTable.indices}. */
637
+ readonly ranges: Uint32Array;
638
+ /** Per cluster, the sphere its own error is projected through. */
639
+ readonly sourceSpheres: Float32Array;
640
+ }
641
+ interface IClusteredMeshOptions {
642
+ /**
643
+ * Screen-space error a cluster may show, in pixels, before its children are drawn instead.
644
+ *
645
+ * One pixel is the honest default: the point of the technique is that what the camera cannot
646
+ * resolve is never submitted. Raising it trades fidelity for triangles, and the number is the
647
+ * game's to choose.
648
+ */
649
+ readonly errorPixels?: number;
650
+ /**
651
+ * How far the camera must move, in the mesh's own units, before the cut is taken again.
652
+ *
653
+ * Popping is a defect, not a tuning parameter: a camera standing still and breathing must not
654
+ * flip a cluster back and forth. Below this the previous cut is kept, which is always a cut some
655
+ * camera would have chosen and therefore always watertight. Default is a thousandth of the mesh's
656
+ * radius.
657
+ */
658
+ readonly recutDistance?: number;
659
+ }
660
+ declare class ClusteredMesh extends Mesh {
661
+ #private;
662
+ /** The baked payload. Read-only at run time; the bake is the only thing that writes it. */
663
+ readonly table: IClusterTable;
664
+ /** Screen-space error budget in pixels. Writable — it is the game's call. */
665
+ errorPixels: number;
666
+ /** Camera movement, in the mesh's own units, below which the previous cut is kept. */
667
+ recutDistance: number;
668
+ constructor(geometry: BufferGeometry, surface: Material | Material[], table: IClusterTable, options?: IClusteredMeshOptions);
669
+ /** Clusters in the current cut. */
670
+ get drawnClusters(): number;
671
+ /** Triangles the current cut submits. */
672
+ get drawnTriangles(): number;
673
+ /**
674
+ * Chooses this frame's cut and compacts it into one index range.
675
+ *
676
+ * Called by the game before it renders, not from `onBeforeRender`: an empty cut has to skip the
677
+ * draw rather than submit a zero-count one, and by the time three calls `onBeforeRender` the draw
678
+ * is already on the list. A mesh this leaves invisible is made visible again by the next call,
679
+ * which is why the call belongs in the frame loop rather than in the renderer.
680
+ *
681
+ * @returns triangles the mesh will draw.
682
+ */
683
+ update(camera: Camera, viewportHeight: number): number;
684
+ }
685
+ /**
686
+ * Takes every clustered mesh and every clustered batch under `root` through this frame's cut.
687
+ *
688
+ * The engine calls this itself, once a frame, before the render — virtual geometry ships on and a
689
+ * game that has to remember to call something has not been given it. A scene holding neither costs
690
+ * one traversal that finds nothing.
691
+ *
692
+ * @returns triangles the clustered meshes and batches will submit.
693
+ */
694
+ declare function updateClusteredMeshes(root: {
695
+ traverse(callback: (object: object) => void): void;
696
+ }, camera: Camera, viewportHeight: number): number;
697
+
698
+ /**
699
+ * Many copies of one clustered body, each drawn at the detail its own distance earns.
700
+ *
701
+ * `InstancedBatch` collapses repeated props into one draw; this does the same for a body that
702
+ * carries a cluster DAG, and adds the part `InstancedBatch` cannot do — a copy twelve metres away
703
+ * and a copy two hundred metres away do not draw the same triangles.
704
+ *
705
+ * **Why instances are bucketed rather than cut one by one.** One indexed draw has one index range,
706
+ * and multi-draw indirect is not portably available on this stack, so *n* different cuts would mean
707
+ * *n* draws and *n* index buffers — on four hundred boulders that is a gigabyte of index data to
708
+ * save vertex work. Instead the copies are grouped by distance, one cut is taken per occupied
709
+ * group, and each group draws as one instanced draw. Every group's cut is a real cut of the DAG and
710
+ * therefore watertight; the group is cut at the distance of its *nearest* member, so no copy is ever
711
+ * drawn coarser than its own distance allows — only finer, by at most the width of one group.
712
+ *
713
+ * Geometry, surface and every transform are the game's, exactly as with `InstancedBatch`.
714
+ */
715
+ /** One copy's transform, in the units the geometry was authored in. */
716
+ interface IClusteredPlacement {
717
+ readonly position: readonly [number, number, number];
718
+ readonly rotation?: readonly [number, number, number];
719
+ readonly scale?: readonly [number, number, number] | number;
720
+ }
721
+ interface IClusteredBatchOptions {
722
+ /**
723
+ * How far the camera must move, as a fraction of its distance to the nearest copy, before the
724
+ * cut is taken again. Default 1%.
725
+ *
726
+ * The engine cuts every batch every frame, so this is the difference between a walk that costs a
727
+ * few hundred cluster tests and one that costs half a million. A camera that has not moved
728
+ * meaningfully keeps the previous cut, which is always a cut some camera would have chosen and
729
+ * therefore always watertight.
730
+ */
731
+ readonly recutFraction?: number;
732
+ /**
733
+ * Ratio between one distance group and the next, above 1.
734
+ *
735
+ * Narrower groups follow each copy's own distance more closely and cost one more draw each.
736
+ * 1.25 is the default: about a dozen groups across a scene that spans a few hundred metres.
737
+ */
738
+ readonly distanceRatio?: number;
739
+ /** Screen-space error budget in pixels, as {@link ClusteredMesh}. Default 1. */
740
+ readonly errorPixels?: number;
741
+ /** The shape every copy draws, carrying the baked cluster table. */
742
+ readonly geometry: BufferGeometry;
743
+ /** The surface every copy draws with, supplied by the game and used by reference. */
744
+ readonly material: Material;
745
+ /** The bake, exactly as `TN_virtual_geometry` stores it. */
746
+ readonly table: IClusterTable;
747
+ }
748
+ interface IClusteredBatchBuildOptions {
749
+ readonly castShadow?: boolean;
750
+ readonly name?: string;
751
+ readonly parent: Object3D;
752
+ readonly receiveShadow?: boolean;
753
+ }
754
+ declare class ClusteredBatch {
755
+ #private;
756
+ constructor(options: IClusteredBatchOptions);
757
+ /** How many copies are placed. */
758
+ get count(): number;
759
+ /** Draws this batch will submit — one per occupied distance group. */
760
+ get drawCalls(): number;
761
+ /** Triangles the current cut submits, over every copy. */
762
+ get drawnTriangles(): number;
763
+ /** Adds one copy. Returns its index, so the game can move it later. */
764
+ place(placement: IClusteredPlacement): number;
765
+ /** Attaches the batch to the scene. Nothing draws until {@link ClusteredBatch.update} runs. */
766
+ build(options: IClusteredBatchBuildOptions): Object3D;
767
+ /**
768
+ * Chooses this frame's cut for every distance group.
769
+ *
770
+ * @returns triangles the batch will submit.
771
+ */
772
+ update(camera: Camera, viewportHeight: number): number;
773
+ }
774
+
775
+ /** The atlas has one copied edge texel on either side of each packed SH sub-volume. */
776
+ declare const ATLAS_PADDING = 1;
777
+ /** The machine-readable marker emitted whenever the probe state changes. */
778
+ declare const PROBE_VOLUME_MARKER = "TN_PROBE_VOLUME";
779
+ type IVector3Like$1 = {
780
+ readonly x: number;
781
+ readonly y: number;
782
+ readonly z: number;
783
+ };
784
+ type IProbePosition = Vector3 | Node<"vec3">;
785
+ /** One RGB L2 spherical-harmonic coefficient, in the upstream probe ordering. */
786
+ interface IProbeVolumeCoefficient {
787
+ readonly r: number;
788
+ readonly g: number;
789
+ readonly b: number;
790
+ }
791
+ /** Probe density in probes per world unit, either isotropic or per-axis. */
792
+ type ProbeVolumeDensity = number | readonly [number, number, number] | IVector3Like$1;
793
+ interface IProbeVolumeOptions {
794
+ /** World-space bounds; the volume does not move with the object after construction. */
795
+ readonly bounds: Box3 | {
796
+ readonly min: IVector3Like$1;
797
+ readonly max: IVector3Like$1;
41
798
  };
42
- readonly window?: {
43
- readonly title?: string;
44
- readonly width?: number;
45
- readonly height?: number;
46
- readonly resizable?: boolean;
799
+ /** Probe spacing expressed as probes per world unit. */
800
+ readonly density: ProbeVolumeDensity;
801
+ /** Optional device limit supplied by a host that knows it before construction. */
802
+ readonly maxTextureDimension3D?: number;
803
+ /** Alias for integrations that expose the WebGPU limit under a device-oriented name. */
804
+ readonly deviceTextureLimit?: number;
805
+ /** Maximum wall-clock work per render phase, in milliseconds. */
806
+ readonly bakeBudgetMs?: number;
807
+ /** Additional guard that keeps a clock-less host from processing an unbounded queue. */
808
+ readonly maxWorkItemsPerFrame?: number;
809
+ /** Resolution of each captured cube face. The static-lighting default is intentionally small. */
810
+ readonly cubemapSize?: number;
811
+ readonly near?: number;
812
+ readonly far?: number;
813
+ /** Additional indirect passes after the direct-light pass. */
814
+ readonly bounces?: number;
815
+ /** Injectable clock for deterministic scheduling tests. */
816
+ readonly now?: () => number;
817
+ readonly report?: (line: string) => void;
818
+ }
819
+ interface IProbeVolumeBakeProgress {
820
+ readonly completed: number;
821
+ readonly total: number;
822
+ readonly fraction: number;
823
+ readonly probesCompleted: number;
824
+ readonly probesTotal: number;
825
+ readonly pass: number;
826
+ readonly passes: number;
827
+ }
828
+ interface IProbeVolumeObservation {
829
+ readonly marker: typeof PROBE_VOLUME_MARKER;
830
+ readonly status: "unbaked" | "baking" | "ready";
831
+ readonly stale: boolean;
832
+ readonly unbaked: boolean;
833
+ /** `null` means no completed bake has established an age yet. */
834
+ readonly stalenessFrames: number | null;
835
+ readonly probeCount: number;
836
+ readonly atlasBytes: number;
837
+ readonly atlas: {
838
+ readonly width: number;
839
+ readonly height: number;
840
+ readonly depth: number;
47
841
  };
842
+ readonly bakeProgress: IProbeVolumeBakeProgress;
843
+ /** Wall-clock time spent by the most recent incremental render-phase slice. */
844
+ readonly bakeCostMs: number;
845
+ readonly bakeBudgetMs: number;
846
+ /** True while pass zero samples a black texture instead of a previous bake. */
847
+ readonly samplingIsolated: boolean;
848
+ }
849
+ /** Read a complete marker-shaped observation without accepting malformed data. */
850
+ declare function readProbeVolumeObservation(value: unknown): IProbeVolumeObservation | undefined;
851
+ /**
852
+ * A WebGPU irradiance probe volume.
853
+ *
854
+ * The volume owns placement, GPU bake scheduling and a single padded atlas. It owns no light,
855
+ * material or colour: every coefficient comes from the scene rendered by its cube cameras. Add it
856
+ * through `ctx.add()` so `process()` runs in the render phase measured by `FrameBudget`.
857
+ *
858
+ * Bakes are static-lighting-first and explicit. Call `requestBake(scene)` after lights and static
859
+ * geometry are authored; a completed bake is reused until the game requests another one.
860
+ */
861
+ declare class ProbeVolume extends Object3D implements IComputeDriven$1 {
862
+ #private;
863
+ readonly isProbeVolume = true;
864
+ readonly processCadence: "render";
865
+ readonly warmupNodes: readonly unknown[];
866
+ readonly boundingBox: Box3;
867
+ readonly resolution: Vector3;
868
+ constructor(options: IProbeVolumeOptions);
869
+ get texture(): Data3DTexture;
870
+ get atlasDepth(): number;
871
+ get atlasBytes(): number;
872
+ get probeCount(): number;
873
+ get released(): boolean;
874
+ get atlasData(): Float32Array;
875
+ get observation(): IProbeVolumeObservation;
876
+ /** Attach the active renderer; only WebGPU has the 3D render-target contract this class needs. */
877
+ attachRenderer(renderer: IRendererLike): void;
878
+ /** Start or coalesce an incremental static bake for a scene. */
879
+ requestBake(scene: Scene, options?: {
880
+ readonly bounces?: number;
881
+ }): Promise<void>;
882
+ /** Alias that reads naturally at call sites that want an awaitable bake request. */
883
+ bake(scene: Scene, options?: {
884
+ readonly bounces?: number;
885
+ }): Promise<void>;
886
+ bake(renderer: IRendererLike, scene: Scene, options?: {
887
+ readonly bounces?: number;
888
+ }): Promise<void>;
48
889
  /**
49
- * What the generated loading screen reads.
890
+ * Return the L2 irradiance node for a world position and world normal.
50
891
  *
51
- * These are declarations, not a renderer: `src/render/loading.ts` is your source and it is the
52
- * only thing that draws them, so a look this cannot express is a file you edit rather than an
53
- * option we add. Deleting that file still opts out of the screen entirely.
892
+ * `sample()` with no arguments is the material-friendly form and reads `positionWorld` and
893
+ * `normalWorld`. Passing numeric vectors is reserved for diagnostics and deterministic tests;
894
+ * it evaluates the same SH coefficients held by the atlas packer.
54
895
  */
55
- readonly loading?: {
56
- /** Image drawn centred above the bar, project-relative like `public/logo.png`. */
57
- readonly image?: string;
58
- readonly backdropColor?: string;
59
- readonly trackColor?: string;
60
- readonly progressColor?: string;
61
- /** False draws the backdrop and image with no bar. */
62
- readonly showProgressBar?: boolean;
896
+ sample(): Node<"vec3">;
897
+ sample(position: Vector3, normal: Vector3): Vector3;
898
+ sample(position: IProbePosition, normal?: Vector3 | Node<"vec3">): Vector3 | Node<"vec3">;
899
+ sampleIrradiance(position: Vector3, normal: Vector3): Vector3;
900
+ /** The GPU graph used by `sample`; exposed so generated materials can compose it explicitly. */
901
+ sampleNode(position?: Node<"vec3">, normal?: Node<"vec3">): Node<"vec3">;
902
+ /** Internal data seam used by unit/conformance fixtures to seed a known SH atlas without a GPU. */
903
+ setProbeCoefficients(ix: number, iy: number, iz: number, coefficients: readonly IProbeVolumeCoefficient[]): void;
904
+ /** One bounded render-phase slice. The game loop calls this through IComputeDriven. */
905
+ process(renderer: IRendererLike): void;
906
+ detach(): void;
907
+ get paddedSlices(): number;
908
+ get maximumDimension(): number;
909
+ get totalWork(): number;
910
+ probePosition(ix: number, iy: number, iz: number, target?: Vector3): Vector3;
911
+ }
912
+
913
+ /**
914
+ * Renderer-independent bookkeeping for virtual shadow pages.
915
+ *
916
+ * Four pieces, none of which know a GPU exists: stable virtual page addresses and a bounded
917
+ * physical page pool with LRU eviction (`PhysicalPagePool`), camera-centred clip windows snapped
918
+ * to whole pages in a light-space basis (`DirectionalClipmap`), the pages a frame needs from
919
+ * receiver points and visible caster bounds (`ReceiverDemandPass`), and the pages a moving caster
920
+ * dirties (`ShadowInvalidationTracker`). `VirtualShadowNode` in `virtual-shadow.ts` is the only
921
+ * consumer; everything here is pure so its rules are provable in a node-environment spec.
922
+ *
923
+ * Fail closed: malformed input throws at the boundary instead of producing a page that samples
924
+ * nothing.
925
+ */
926
+ /** A plain `{x, y, z}` triple; three's `Vector3` satisfies it without a copy. */
927
+ interface IVector3Like {
928
+ readonly x: number;
929
+ readonly y: number;
930
+ readonly z: number;
931
+ }
932
+ /** An axis-aligned world-space box as two corners. */
933
+ interface IBoundsLike {
934
+ readonly min: IVector3Like;
935
+ readonly max: IVector3Like;
936
+ }
937
+ /** A point in the light-space basis: `u` and `v` across the light, `w` toward it. */
938
+ interface ILightSpacePoint {
939
+ readonly u: number;
940
+ readonly v: number;
941
+ readonly w: number;
942
+ }
943
+ /** The address of one virtual page: a clip level and integer page coordinates. */
944
+ interface IVirtualPageAddress {
945
+ readonly level: number;
946
+ readonly x: number;
947
+ readonly y: number;
948
+ readonly key: string;
949
+ }
950
+ /** One clip level's window of `pagesPerAxis²` virtual pages around the centre. */
951
+ interface IClipWindow {
952
+ readonly level: number;
953
+ readonly extent: number;
954
+ readonly pageWorldSize: number;
955
+ readonly minX: number;
956
+ readonly minY: number;
957
+ readonly maxX: number;
958
+ readonly maxY: number;
959
+ }
960
+ interface IDirectionalClipmapOptions {
961
+ /** Unit-free direction *toward* the source; normalised here. */
962
+ readonly direction: IVector3Like;
963
+ /** Half-width of each level's window in world units, finest first. */
964
+ readonly clipExtents: readonly number[];
965
+ readonly pagesPerAxis: number;
966
+ /** Fraction of an extent inside which a point selects that level; `(0, 1]`. */
967
+ readonly selectionGuard?: number;
968
+ }
969
+ /**
970
+ * Camera-centred clip windows in a light-space basis, snapped to whole pages.
971
+ *
972
+ * The basis is right-handed (`U × V = W`). A page camera placed along `+W` with `up = V` then
973
+ * renders screen X = `V × W` = `+U`, which is the axis the sampler reads the atlas along. The
974
+ * prototype this was ported from built `V = U × W` and every page came out mirrored.
975
+ */
976
+ declare class DirectionalClipmap {
977
+ #private;
978
+ readonly clipExtents: readonly number[];
979
+ readonly pagesPerAxis: number;
980
+ readonly selectionGuard: number;
981
+ readonly levelCount: number;
982
+ basisU: IVector3Like;
983
+ basisV: IVector3Like;
984
+ basisW: IVector3Like;
985
+ centerWorld: IVector3Like;
986
+ centerLight: ILightSpacePoint;
987
+ constructor({ direction, clipExtents, pagesPerAxis, selectionGuard, }: IDirectionalClipmapOptions);
988
+ /** Re-orient the basis; returns true when it changed enough that cached pages are stale. */
989
+ setDirection(direction: IVector3Like): boolean;
990
+ project(worldPoint: IVector3Like): ILightSpacePoint;
991
+ unproject({ u, v, w }: {
992
+ u: number;
993
+ v: number;
994
+ w?: number;
995
+ }): IVector3Like;
996
+ updateCenter(worldPoint: IVector3Like): readonly IClipWindow[];
997
+ getWindow(level: number): IClipWindow;
998
+ pageWorldSize(level: number): number;
999
+ containsPage(level: number, x: number, y: number): boolean;
1000
+ worldToPage(worldPoint: IVector3Like, level: number): IVirtualPageAddress;
1001
+ /** The finest level whose guarded extent contains the point, else the coarsest. */
1002
+ selectLevel(worldPoint: IVector3Like): number;
1003
+ /** Light-space extent and world centre (at `w = 0`) of one page. */
1004
+ pageBounds(level: number, x: number, y: number): {
1005
+ level: number;
1006
+ x: number;
1007
+ y: number;
1008
+ minU: number;
1009
+ minV: number;
1010
+ maxU: number;
1011
+ maxV: number;
1012
+ centerU: number;
1013
+ centerV: number;
1014
+ pageWorldSize: number;
1015
+ centerWorld: IVector3Like;
63
1016
  };
64
- readonly nativeEntry?: string;
65
- readonly renderer?: {
66
- readonly preferWebGPU?: boolean;
1017
+ boundsToPageRange(bounds: IBoundsLike, level: number): {
1018
+ minX: number;
1019
+ maxX: number;
1020
+ minY: number;
1021
+ maxY: number;
1022
+ };
1023
+ boundsToPageKeys(bounds: IBoundsLike, level: number): string[];
1024
+ windowPages(level: number): IVirtualPageAddress[];
1025
+ }
1026
+ /**
1027
+ * Remembers each tracked caster's last bounds and dirties every page the old and new bounds
1028
+ * cover on every level when they change. Unchanged bounds dirty nothing.
1029
+ */
1030
+ declare class ShadowInvalidationTracker {
1031
+ #private;
1032
+ readonly clipmap: DirectionalClipmap;
1033
+ constructor(clipmap: DirectionalClipmap);
1034
+ get trackedCount(): number;
1035
+ /** Record a caster's current bounds; returns true when pages were dirtied. */
1036
+ update(id: number | string, bounds: IBoundsLike): boolean;
1037
+ has(id: number | string): boolean;
1038
+ remove(id: number | string): boolean;
1039
+ /** Dirty the current coverage of every tracked caster; returns the pending key count. */
1040
+ invalidateAll(): number;
1041
+ /** Drop trackers whose id is not in `liveIds`, dirtying the pages they covered. */
1042
+ prune(liveIds: ReadonlySet<number | string>): number;
1043
+ consumeInvalidatedKeys(): Set<string>;
1044
+ clear(): void;
1045
+ }
1046
+
1047
+ /**
1048
+ * Options for {@link VirtualShadowNode}. Every value is a mechanism parameter; the light's own
1049
+ * `shadow` keeps bias, normalBias, intensity, map type and filter, exactly as with a stock shadow.
1050
+ */
1051
+ interface IVirtualShadowOptions {
1052
+ /**
1053
+ * Half-width of each clip level's window in world units, finest first and strictly
1054
+ * increasing. Default `[16, 48, 144]`: three windows, each three times wider than the last.
1055
+ */
1056
+ readonly clipExtents?: readonly number[];
1057
+ /** Texels per level edge. Default: the light's `shadow.mapSize.width`. */
1058
+ readonly mapSize?: number;
1059
+ /**
1060
+ * Texels per edge of each level's mover map — the map tracked casters draw into every frame.
1061
+ * Default: half of `mapSize`, never below 256. Movers are few and close, so half the texels
1062
+ * over the same window reads as the same shadow at a quarter of the fill.
1063
+ */
1064
+ readonly moverMapSize?: number;
1065
+ /**
1066
+ * Fraction of a level's extent inside which a fragment still selects that level, `(0, 1]`,
1067
+ * default 0.9 — the outer ring falls through to the next level so the edge is never sampled.
1068
+ */
1069
+ readonly selectionGuard?: number;
1070
+ /** How far behind the window centre each level camera sits, in world units. Default 200. */
1071
+ readonly lightDistance?: number;
1072
+ /** Depth range each level camera covers past its centre, in world units. Default 400. */
1073
+ readonly depthRange?: number;
1074
+ /** Print the `TN_VIRTUAL_SHADOW` line every `markerEvery` frames; `false` silences it. Default 300. */
1075
+ readonly marker?: boolean | number;
1076
+ }
1077
+ /** Per-frame counters, readable any time through {@link VirtualShadowNode.stats}. */
1078
+ interface IVirtualShadowStats {
1079
+ readonly frame: number;
1080
+ readonly levels: number;
1081
+ /** Levels whose window moved this frame and were re-rendered. */
1082
+ readonly moved: number;
1083
+ /** Levels re-rendered because `invalidateAll()` or explicit tracker invalidation asked for it. */
1084
+ readonly invalidated: number;
1085
+ /** Tracked casters, as of this frame. */
1086
+ readonly movers: number;
1087
+ /** Mover maps rendered this frame: one per level when at least one caster is tracked. */
1088
+ readonly moverRenders: number;
1089
+ /** Levels served from their cached map this frame. */
1090
+ readonly cached: number;
1091
+ /** Levels rendered this frame, for any reason. */
1092
+ readonly rendered: number;
1093
+ /** Fraction of levels served from cache over the node's lifetime. */
1094
+ readonly reuseRatio: number;
1095
+ }
1096
+ declare const VIRTUAL_SHADOW_MARKER = "TN_VIRTUAL_SHADOW";
1097
+ /**
1098
+ * The object layer tracked casters are enabled on, so each level's mover camera sees only them.
1099
+ * Keep it free of other uses; the main camera never needs it (tracked objects keep layer 0).
1100
+ */
1101
+ declare const VIRTUAL_SHADOW_MOVER_LAYER = 29;
1102
+ /**
1103
+ * One directional shadow for a whole open world: camera-centred clip levels, each snapped to its
1104
+ * own texel grid and re-rendered only when its window moves. Movers never touch that cache: a
1105
+ * tracked caster draws into a second, per-level mover map every frame, and a fragment takes the
1106
+ * darker of the two — so a walking stag costs one small render of itself, not a render of the wood.
1107
+ *
1108
+ * Plugs into three's own slot, so every material in the scene receives it with no other change:
1109
+ * `light.shadow.shadowNode = new VirtualShadowNode(light, { clipExtents: [16, 48, 144] })`.
1110
+ *
1111
+ * What it owns is mechanism: level windows, texel snapping, per-level caching, invalidation,
1112
+ * level selection and the statistics. The light's `shadow` keeps bias, normal bias, intensity,
1113
+ * map type and filter, and those source settings are mirrored into each stock level node before
1114
+ * rendering. Per-level map sizes, cameras, `autoUpdate` and `needsUpdate` are owned by this node.
1115
+ * Each level is rendered by the stock {@link ShadowNode} through the renderer's shadow-map type,
1116
+ * so the look is the same code path a plain shadow uses.
1117
+ *
1118
+ * Ported from the virtual-shadow-map prototype's clipmap and invalidation; the sparse page atlas
1119
+ * is deliberately not the first cut — a page needs the scene rendered once per page, and on a
1120
+ * forest of hundreds of instanced meshes three level renders are cheaper than twenty-four page
1121
+ * renders. The page pool and demand pass in `virtual-shadow-pages.ts` stay ready for it.
1122
+ *
1123
+ * @situation crisp shadows close to the player across a large outdoor level
1124
+ * @situation shadow map too coarse over a big terrain
1125
+ * @situation one directional light shadow for a whole open world
1126
+ * @situation shadows shimmer when the camera moves
1127
+ * @constraint the light must be a DirectionalLight with `castShadow` and a target in the scene
1128
+ * @constraint clipExtents are half-widths in world units, finest first, strictly increasing
1129
+ * @constraint call `trackCaster(object)` for movers; it enables layer `VIRTUAL_SHADOW_MOVER_LAYER` on the object and its descendants, tracking or untracking refreshes cached levels once, and subsequent mover movement refreshes only when a window moves
1130
+ * @override bias, biasNode, normalBias, intensity, radius, blurSamples, mapType and filterNode stay on `light.shadow`; mapSize and the other options here have defaults
1131
+ * @example
1132
+ * const sun = new DirectionalLight(0xffffff, 3);
1133
+ * sun.castShadow = true;
1134
+ * sun.shadow.shadowNode = new VirtualShadowNode(sun, { clipExtents: [12, 40, 120] });
1135
+ */
1136
+ declare class VirtualShadowNode extends ShadowBaseNode {
1137
+ #private;
1138
+ static get type(): string;
1139
+ readonly options: Required<Omit<IVirtualShadowOptions, "marker">> & {
1140
+ readonly markerEvery: number;
67
1141
  };
1142
+ readonly clipmap: DirectionalClipmap;
1143
+ /**
1144
+ * Compatibility handle for explicit page invalidation. Automatic caster motion uses mover maps;
1145
+ * callers that already update this tracker still invalidate the affected cached levels.
1146
+ */
1147
+ readonly tracker: ShadowInvalidationTracker;
1148
+ constructor(light: DirectionalLight, options?: IVirtualShadowOptions);
1149
+ /** The per-frame counters, as of the last `updateBefore`. */
1150
+ get stats(): IVirtualShadowStats;
1151
+ /** The stock shadow nodes behind each level, for diagnostics. */
1152
+ get levelNodes(): readonly Node[];
1153
+ /** The stock shadow nodes behind each level's mover map, for diagnostics. */
1154
+ get moverNodes(): readonly Node[];
1155
+ /** The placeholder lights, one per level; exposed for tests and debug views. */
1156
+ get levelLights(): readonly Object3D[];
1157
+ /**
1158
+ * Make an object a mover: it leaves the cached level maps and draws into every level's mover
1159
+ * map each frame, so its shadow follows it without a level render. Static geometry never
1160
+ * needs this — a level re-renders whenever its window moves anyway.
1161
+ */
1162
+ trackCaster(object: Object3D): string;
1163
+ untrackCaster(objectOrId: Object3D | string): boolean;
1164
+ /** Force every level to re-render on the next frame — a tree fell, a door opened. */
1165
+ invalidateAll(): void;
1166
+ setup(builder: NodeBuilder): Node | null | undefined;
1167
+ updateBefore(frame: NodeFrame): undefined;
1168
+ dispose(): void;
1169
+ }
1170
+ /**
1171
+ * Parse a `TN_VIRTUAL_SHADOW` console line back into its complete stats, or `undefined`.
1172
+ *
1173
+ * @situation inspect virtual shadow cache and mover counters from a renderer log
1174
+ * @constraint non-marker lines and markers with incomplete or non-numeric stats return `undefined`
1175
+ * @example
1176
+ * const stats = readVirtualShadowMarker(line);
1177
+ * if (stats !== undefined) console.log(stats.reuseRatio);
1178
+ */
1179
+ declare function readVirtualShadowMarker(line: string): IVirtualShadowStats | undefined;
1180
+
1181
+ /**
1182
+ * Two load-time mechanisms every streaming game writes by hand, and gets wrong in the same places.
1183
+ *
1184
+ * A game that shows a loading curtain and builds its world behind it has two loops that decide how
1185
+ * long the curtain stays up, and neither is about how anything looks: the loop that **fetches** the
1186
+ * models and the loop that **attaches** the finished objects to the scene. Written naively, the
1187
+ * first is serial and the second is one-object-per-frame, and both are slow for reasons that have
1188
+ * nothing to do with the game.
1189
+ *
1190
+ * Measured in a real game (a 190 m valley, ~70 GLBs, ~400 attached objects), on the same build and
1191
+ * the same content:
1192
+ *
1193
+ * | Attach loop | Detail tier |
1194
+ * | ------------ | ----------- |
1195
+ * | 1 per frame | 16.5 s |
1196
+ * | 6 per frame | 11.26 s |
1197
+ * | 24 per frame | 9.39 s |
1198
+ * | 256 per frame| **6.89 s** |
1199
+ * | all at once | 7.18 s |
1200
+ *
1201
+ * and the fetch loop: 52 models one at a time took **38.4 s**; six at a time took **8.8 s**.
1202
+ *
1203
+ * Neither of these decides how anything looks. `addInSlices` never creates an object and never
1204
+ * chooses where it goes — it is handed a list and the game's own `add`. `loadAll` never chooses
1205
+ * what to load — it is handed a list and the game's own `load`. Geometry, material, colour,
1206
+ * texture, curve and timing stay with the game in both, and a game can change its appearance
1207
+ * completely without editing either.
1208
+ *
1209
+ * What they are not is code a game can get right portably by itself, which is why they live here.
1210
+ * Yielding correctly is a platform seam: awaiting `requestAnimationFrame` deadlocks a host whose
1211
+ * frames are pumped by the code doing the waiting, racing it against a timer reorders the frame
1212
+ * sequence a harness observes, and a hard-coded `setTimeout(16)` is neither a frame on the native
1213
+ * runtime nor a frame on a machine that cannot hold 60. See `yieldToHost` in `warmup.ts` for the
1214
+ * two wrong versions that came before the one both of these use.
1215
+ */
1216
+ /** How far a sliced attachment has got, reported once per slice. */
1217
+ interface IAddInSlicesProgress {
1218
+ /** Objects attached so far. Never greater than `total`. */
1219
+ readonly added: number;
1220
+ /** Objects the run was given. Known before the first slice. */
1221
+ readonly total: number;
1222
+ }
1223
+ interface IAddInSlicesOptions {
1224
+ /**
1225
+ * Objects attached between presented frames. Default 256.
1226
+ *
1227
+ * The default is large because the reason it used to be small has been taken over by something
1228
+ * better. One per frame was right when nothing else compiled the world: the renderer built a few
1229
+ * newly visible pipelines per presented frame instead of all of them inside one multi-second
1230
+ * frame. It is not right once the framework warms the held scene (`warmUpScene`) — the compile is
1231
+ * then paid either way and the slice only chooses where, so a small slice buys nothing and costs
1232
+ * one present per object.
1233
+ *
1234
+ * 256 is where the measured curve flattens without giving up the yields entirely: over a few
1235
+ * hundred objects it still presents a handful of frames, so a browser watchdog cannot see a hung
1236
+ * page, and it costs nothing against attaching everything in one go.
1237
+ */
1238
+ readonly sliceSize?: number;
1239
+ /** Called once per slice, and once more for a partial final slice. */
1240
+ readonly onProgress?: (progress: IAddInSlicesProgress) => void;
1241
+ /**
1242
+ * How the run hands the loop back to the host between slices. Defaults to yielding one
1243
+ * macrotask, which is one native-runtime loop iteration and therefore one presented frame.
1244
+ */
1245
+ readonly yieldFrame?: () => Promise<void>;
1246
+ /**
1247
+ * Asked before every object; a false answer stops the run.
1248
+ *
1249
+ * This is how a scene that can be torn down mid-attach stays correct. A generation invalidated
1250
+ * by a restart, a scene change, or an HMR reload must stop attaching into a graph that is no
1251
+ * longer current, and it must do so **without throwing** — a throw here is indistinguishable
1252
+ * from a real attachment failure at the catch site, and games end up reporting a teardown as an
1253
+ * asset error. The stop is reported instead, as `stopped` on the report.
1254
+ */
1255
+ readonly while?: () => boolean;
1256
+ /** Silences the `TN_ADD_SLICES` marker. Never silences the report. Default true (on). */
1257
+ readonly marker?: boolean;
1258
+ }
1259
+ /** What a sliced attachment did, so a caller can report it rather than assume it. */
1260
+ interface IAddInSlicesReport {
1261
+ /** Objects actually handed to `add`. */
1262
+ readonly added: number;
1263
+ /** Objects the run was given. */
1264
+ readonly total: number;
1265
+ /** Slices the work was cut into, and therefore the frames the loop got to present, plus one. */
1266
+ readonly slices: number;
1267
+ /** Wall-clock milliseconds the run took, attaching and yielding together. */
1268
+ readonly elapsedMs: number;
1269
+ /** The slice size actually used — the default, or the one the game chose. */
1270
+ readonly sliceSize: number;
1271
+ /** Whether that slice size came from the game rather than from the default. */
1272
+ readonly sliceSizeOverridden: boolean;
1273
+ /** True when `while` ended the run early. `added` is then less than `total`. */
1274
+ readonly stopped: boolean;
1275
+ }
1276
+ /** How far a bounded-concurrency load has got, reported once per settled item. */
1277
+ interface ILoadAllProgress {
1278
+ /** Loads that have resolved. */
1279
+ readonly settled: number;
1280
+ /** Loads the run was given. Known before the first one starts. */
1281
+ readonly total: number;
1282
+ }
1283
+ interface ILoadAllOptions {
1284
+ /**
1285
+ * Loads in flight at once. Default 6.
1286
+ *
1287
+ * Bounded rather than unbounded, because a full-width fan-out coalesces its completion callbacks
1288
+ * into 100-200 ms tasks and freezes the loading screen it is filling — which reads as the hang
1289
+ * the concurrency was added to remove. Six is also the per-host connection limit a browser
1290
+ * applies to HTTP/1.1, so a larger number frequently buys queueing rather than parallelism.
1291
+ */
1292
+ readonly concurrency?: number;
1293
+ /** Called after each load settles. */
1294
+ readonly onProgress?: (progress: ILoadAllProgress) => void;
1295
+ /**
1296
+ * How the run hands the loop back to the host after each settled load. Defaults to yielding one
1297
+ * macrotask, which is what keeps a progress bar moving while the lanes are busy.
1298
+ */
1299
+ readonly yieldFrame?: () => Promise<void>;
1300
+ /** Silences the `TN_LOAD_ALL` marker. Never silences `onProgress`. Default true (on). */
1301
+ readonly marker?: boolean;
1302
+ }
1303
+ /**
1304
+ * Attach a built list of objects to the scene in slices, presenting a frame between each.
1305
+ *
1306
+ * The objects are already built and already the game's; this decides only *when* each one joins
1307
+ * the graph. Order is the list's order, and there is no option to change it — an attach loop that
1308
+ * reordered its input would change which object a positional lookup finds.
1309
+ *
1310
+ * @situation add hundreds of built objects to the scene without one multi-second frame
1311
+ * @situation stream a detail tier in behind a loading curtain without the page looking hung
1312
+ * @example
1313
+ * const report = await addInSlices(detailObjects, (object) => ctx.add(object), {
1314
+ * onProgress: ({ added, total }) => setProgress(added / total),
1315
+ * while: () => generation.live,
1316
+ * });
1317
+ */
1318
+ declare function addInSlices<T>(objects: Iterable<T>, add: (object: T, index: number) => void, options?: IAddInSlicesOptions): Promise<IAddInSlicesReport>;
1319
+ /**
1320
+ * Load a list with bounded concurrency, and hand the results back **in the input's order**.
1321
+ *
1322
+ * The ordering is the whole point and has no override. `Promise.all` already keeps order but runs
1323
+ * everything at once; a hand-rolled worker pool bounds the lanes but pushes results as they land,
1324
+ * so the array comes back in completion order — whatever the network happened to return. A game
1325
+ * that picks from that list positionally then places a different model in the same spot on every
1326
+ * load, and its world is never the same twice. That defect shipped in a real game and is why this
1327
+ * writes each result to its item's own index and never appends.
1328
+ *
1329
+ * Fails closed like `Promise.all`: the first rejection rejects the call, and no lane starts a load
1330
+ * it had not already begun.
1331
+ *
1332
+ * @situation load many models or textures in parallel instead of one at a time
1333
+ * @situation keep a loading screen moving while a list of assets downloads
1334
+ * @example
1335
+ * const species = await loadAll(names, (name) => ctx.assets.model(`flora/${name}.glb`), {
1336
+ * onProgress: ({ settled, total }) => setProgress(settled / total),
1337
+ * });
1338
+ */
1339
+ declare function loadAll<TIn, TOut>(items: readonly TIn[], load: (item: TIn, index: number) => Promise<TOut>, options?: ILoadAllOptions): Promise<TOut[]>;
1340
+
1341
+ /** One band of the spectrum, drawn on its own patch. */
1342
+ interface ISpectralOceanCascade {
1343
+ /** The world-space edge length, in metres, this cascade's grid tiles across. */
1344
+ readonly patchSize: number;
1345
+ }
1346
+ interface ISpectralOceanOptions {
1347
+ /** Grid resolution per cascade. A power of two; the transform has no other shape. */
1348
+ readonly resolution: number;
1349
+ /**
1350
+ * The cascades, largest patch first.
1351
+ *
1352
+ * Each one carries only the wavelengths the next-smaller patch cannot resolve, so the bands do
1353
+ * not overlap and a wave is never counted twice. One cascade is a toy: the join between bands is
1354
+ * where a spectral ocean visibly fails, so there is nothing to look at until there are two.
1355
+ */
1356
+ readonly cascades: readonly ISpectralOceanCascade[];
1357
+ readonly windSpeed: number;
1358
+ /** Wind heading in radians. */
1359
+ readonly windDirection: number;
1360
+ readonly gravity: number;
1361
+ /** Overall spectrum scale. A wave height decision, so the game owns the number. */
1362
+ readonly amplitude: number;
1363
+ /**
1364
+ * How sharply waves align with the wind, as the exponent on the directional spread.
1365
+ *
1366
+ * A spectrum-tuning number with no defensible default, so there is none.
1367
+ */
1368
+ readonly directionality: number;
1369
+ /** Horizontal displacement scale. Zero is a pure heightfield; higher values sharpen crests. */
1370
+ readonly choppiness: number;
1371
+ /** Waves shorter than this are cut off, in metres. */
1372
+ readonly smallWaveCutoff: number;
1373
+ readonly seed: number;
1374
+ /**
1375
+ * Which clock advances the simulation. Defaults to the game's fixed step.
1376
+ *
1377
+ * Fixed, because this sea is something the game reads: `sampleHeight` reports its age in frames,
1378
+ * and a field advanced by the display makes that age mean a different amount of time on every
1379
+ * machine. A game whose ocean is only ever looked at can pass `"render"` and pay for exactly the
1380
+ * frames it draws.
1381
+ */
1382
+ readonly cadence?: "fixed" | "render";
1383
+ /**
1384
+ * The grid the CPU height query is sampled on, per side. Zero disables the query entirely.
1385
+ *
1386
+ * This is not the simulation resolution. It is the size of the only thing copied back off the
1387
+ * GPU, so it is the whole cost of being able to float something: `readbackResolution` squared
1388
+ * floats, every `readbackEveryFrames` frames.
1389
+ */
1390
+ readonly readbackResolution: number;
1391
+ /** Frames between height copies. Ignored when `readbackResolution` is zero. */
1392
+ readonly readbackEveryFrames: number;
1393
+ }
1394
+ /** A height read from the CPU copy, with the age of the frame that produced it. */
1395
+ interface ISpectralOceanHeight {
1396
+ readonly height: number;
1397
+ /**
1398
+ * Frames between the GPU state this height came from and now.
1399
+ *
1400
+ * A spectral ocean cannot offer an exact CPU height — there is no closed form, only texels the
1401
+ * GPU made — so this number is the contract. A caller that ignores it floats a hull on water
1402
+ * that is not the water being drawn, and nothing in the frame says so.
1403
+ */
1404
+ readonly staleFrames: number;
1405
+ }
1406
+ /**
1407
+ * A spectral ocean: cascaded wave spectra, inverse-transformed on the GPU every frame.
1408
+ *
1409
+ * It draws nothing. The game builds its own mesh and its own material and reads
1410
+ * `cascadeDisplacement(index)`; every colour, every foam threshold, every sky this water reflects
1411
+ * is the game's, and none of it can be changed from here.
1412
+ *
1413
+ * What it offers that an analytic wave function cannot is the look. What it cannot offer is an
1414
+ * exact CPU height: there is no closed form, only the texels the GPU produced, so `sampleHeight`
1415
+ * is a throttled copy that is always some frames behind and always says how many. A game that
1416
+ * needs the height to be exact wants an analytic field instead — that is a different contract, and
1417
+ * the reason this class has a different name rather than a flag.
1418
+ */
1419
+ declare class SpectralOcean extends Object3D implements IComputeDriven$1 {
1420
+ #private;
1421
+ readonly resolution: number;
1422
+ readonly cascades: readonly ISpectralOceanCascade[];
1423
+ readonly processCadence: "fixed" | "render";
1424
+ readonly warmupNodes: readonly ComputeNode[];
1425
+ /** Floats copied off the GPU per readback, so a report can state the cost rather than imply it. */
1426
+ readonly readbackFloats: number;
1427
+ constructor(options: ISpectralOceanOptions);
1428
+ get released(): boolean;
1429
+ /** Simulation steps dispatched. A report that cannot count its own dispatches proves nothing. */
1430
+ get steps(): number;
1431
+ /** How old the CPU height copy is, or `undefined` when the height query is switched off. */
1432
+ get staleFrames(): number | undefined;
1433
+ /** The `(displaceX, height, displaceZ, fold)` buffer the game's material reads. */
1434
+ cascadeDisplacement(index: number): StorageBufferNode<"vec4">;
1435
+ /** The world-space tile size of one cascade, which the game's material needs to place it. */
1436
+ cascadePatchSize(index: number): number;
1437
+ /** Seconds of wave time. The game advances it, so a paused game has a paused sea. */
1438
+ advance(seconds: number): void;
1439
+ /**
1440
+ * The surface height at a world position, and how many frames behind it is.
1441
+ *
1442
+ * `undefined` until the first copy lands, and `undefined` forever when the height query was
1443
+ * switched off — never zero, because a hull floating at zero is indistinguishable from a hull
1444
+ * floating at sea level and that is exactly the mistake this must not allow.
1445
+ */
1446
+ sampleHeight(x: number, z: number): ISpectralOceanHeight | undefined;
1447
+ attachRenderer(renderer: IRendererLike): void;
1448
+ process(renderer?: IRendererLike | undefined): void;
1449
+ detach(): void;
68
1450
  }
69
1451
 
70
1452
  interface IGPUParticles3DBuffers {
@@ -77,10 +1459,12 @@ interface IGPUParticles3DOptions {
77
1459
  readonly start: (buffers: IGPUParticles3DBuffers) => ComputeNode;
78
1460
  readonly process: (buffers: IGPUParticles3DBuffers) => ComputeNode;
79
1461
  }
80
- declare class GPUParticles3D extends Sprite {
1462
+ declare class GPUParticles3D extends Sprite implements IComputeDriven$1 {
81
1463
  #private;
82
1464
  readonly amount: number;
83
1465
  readonly buffers: IGPUParticles3DBuffers;
1466
+ readonly processCadence: "render";
1467
+ readonly warmupNodes: readonly ComputeNode[];
84
1468
  emitting: boolean;
85
1469
  constructor(options: IGPUParticles3DOptions);
86
1470
  get released(): boolean;
@@ -90,22 +1474,348 @@ declare class GPUParticles3D extends Sprite {
90
1474
  detach(): void;
91
1475
  }
92
1476
 
1477
+ interface IFluidFieldVector2 {
1478
+ readonly x: number;
1479
+ readonly y: number;
1480
+ }
1481
+ interface IFluidFieldOptions {
1482
+ readonly resolution: number;
1483
+ readonly viscosity?: number;
1484
+ readonly pressureIterations?: number;
1485
+ readonly maxSplats?: number;
1486
+ readonly timeStep?: number;
1487
+ readonly vorticity?: number;
1488
+ readonly splatRadius?: number;
1489
+ }
1490
+ interface IFluidFieldSampler {
1491
+ sample(uv: Node$1<"vec2">): StorageTextureNode;
1492
+ }
1493
+ /**
1494
+ * Run a deterministic GPU fluid field whose data stays available to the game's render graph.
1495
+ *
1496
+ * The class is deliberately a scene object with the compute-driven lifecycle contract: adding it
1497
+ * to a game scene attaches the renderer, warm-up sees every pass, fixed steps dispatch the passes,
1498
+ * and removing it releases every GPU allocation. `dye` and `velocity` are read-only numeric
1499
+ * samplers; the game decides what those values become when drawn.
1500
+ * @situation simulate smoke, fire, fog, wind, or fluid response on a grid
1501
+ * @situation inject a touch, pointer, or gameplay impulse into a fluid field
1502
+ * @situation sample fluid dye or velocity in a game-owned render node
1503
+ * @constraint add the field through `ctx.add` so renderer attachment, fixed-step dispatch, and release are automatic
1504
+ * @constraint `dye` and `velocity` are numeric samplers; appearance stays in the game's `src/render/` code
1505
+ * @override pressureIterations, viscosity, vorticity, and splatRadius tune the solver without changing its pass order
1506
+ * @example const field = new FluidField2D({ resolution: 256, viscosity: 0, pressureIterations: 20 });
1507
+ * ctx.add(field);
1508
+ * field.splat({ x: 0.5, y: 0.5 }, { x: 0.2, y: 0 }, 1);
1509
+ */
1510
+ declare class FluidField2D extends Group {
1511
+ #private;
1512
+ readonly resolution: number;
1513
+ readonly viscosity: number;
1514
+ readonly pressureIterations: number;
1515
+ readonly maxSplats: number;
1516
+ readonly timeStep: number;
1517
+ readonly vorticity: number;
1518
+ readonly splatRadius: number;
1519
+ readonly processCadence: "fixed";
1520
+ readonly warmupNodes: readonly ComputeNode[];
1521
+ readonly velocity: IFluidFieldSampler;
1522
+ readonly dye: IFluidFieldSampler;
1523
+ constructor(options: IFluidFieldOptions);
1524
+ get released(): boolean;
1525
+ get queuedSplats(): number;
1526
+ get steps(): number;
1527
+ get splatsApplied(): number;
1528
+ attachRenderer(renderer: IRendererLike): void;
1529
+ splat(uv: IFluidFieldVector2, velocity: IFluidFieldVector2, amount: number): void;
1530
+ process(renderer?: IRendererLike | undefined): void;
1531
+ detach(): void;
1532
+ }
1533
+
1534
+ type WaveDirection = readonly [number, number] | {
1535
+ readonly x: number;
1536
+ readonly z?: number;
1537
+ readonly y?: number;
1538
+ };
1539
+ interface IWaveFieldWave {
1540
+ readonly amplitude?: number;
1541
+ readonly direction: WaveDirection;
1542
+ readonly wavelength: number;
1543
+ readonly speed: number;
1544
+ readonly phase?: number;
1545
+ readonly steepness?: number;
1546
+ /**
1547
+ * Mark this wave as detail: the graph fades it out with the `fade` node passed to
1548
+ * `heightNode` / `normalNode`, and leaves it at full amplitude everywhere else.
1549
+ *
1550
+ * A wave shorter than the distance one screen pixel covers cannot be resolved, and what it
1551
+ * produces instead is a crawling moire that reads as a repeating pattern. Fading it is the fix.
1552
+ * `sample` on the CPU has no camera and therefore no fade, so CPU and GPU height differ by at
1553
+ * most the summed amplitude of the detail waves — keep them small, or float things on the
1554
+ * non-detail waves alone.
1555
+ */
1556
+ readonly detail?: boolean;
1557
+ }
1558
+ interface IWaveFieldDomainWarp {
1559
+ readonly direction?: WaveDirection;
1560
+ readonly waveVector?: WaveDirection;
1561
+ readonly displacement?: WaveDirection;
1562
+ readonly amplitude?: number;
1563
+ readonly wavelength?: number;
1564
+ readonly speed: number;
1565
+ readonly phase?: number;
1566
+ }
1567
+ interface IWaveFieldOptions {
1568
+ readonly waves: readonly IWaveFieldWave[];
1569
+ readonly domainWarp?: readonly IWaveFieldDomainWarp[];
1570
+ }
1571
+ /** Where, when, and how finely to evaluate the field in a graph. */
1572
+ interface IWaveFieldGraphOptions {
1573
+ /**
1574
+ * The horizontal point to evaluate at, in whatever space the caller wants the answer in.
1575
+ * Defaults to this vertex's local x and z.
1576
+ */
1577
+ readonly point?: Node<"vec2">;
1578
+ /** The clock. Defaults to the field's own `time` uniform. */
1579
+ readonly time?: Node<"float">;
1580
+ /** A 0..1 multiplier on every wave marked `detail`. Omitted, detail waves stay at full size. */
1581
+ readonly fade?: Node<"float">;
1582
+ }
1583
+ interface IWaveFieldSample {
1584
+ readonly height: number;
1585
+ readonly normal: Vector3;
1586
+ }
1587
+ /**
1588
+ * An analytic wave field with one packed parameter source for CPU sampling and TSL displacement.
1589
+ * It owns no geometry or appearance; a game chooses how the returned displacement is drawn.
1590
+ */
1591
+ declare class WaveField {
1592
+ #private;
1593
+ readonly waves: readonly IWaveFieldWave[];
1594
+ readonly domainWarp: readonly IWaveFieldDomainWarp[];
1595
+ readonly parameters: Readonly<Float32Array>;
1596
+ readonly time: three_webgpu.UniformNode<"float", number>;
1597
+ constructor(options: IWaveFieldOptions);
1598
+ /** Update the default graph clock. Explicit sample times remain available for fixed-step code. */
1599
+ setTime(value: number): void;
1600
+ sample(x: number, z: number, time: number): IWaveFieldSample;
1601
+ /** Surface height at a point, as a graph. The scalar half of what `sample` returns. */
1602
+ heightNode(options?: IWaveFieldGraphOptions): Node<"float">;
1603
+ /**
1604
+ * The analytic surface normal at a point, as a graph — the same value `sample` returns, and the
1605
+ * reason a water material needs no hand-written ripple pattern.
1606
+ *
1607
+ * Differencing the height, or stamping a normal map over the surface, is what puts visible
1608
+ * repeats in water: both quantise a field that has none. This differentiates the wave sum
1609
+ * itself, so the normal repeats only where the waves do, which for wavelengths with no common
1610
+ * multiple is nowhere.
1611
+ *
1612
+ * Evaluate it per fragment — pass `point` in world XZ — and the ripples survive at any distance
1613
+ * from the camera, at the cost of the wave sum running per pixel rather than per vertex.
1614
+ */
1615
+ normalNode(options?: IWaveFieldGraphOptions): Node<"vec3">;
1616
+ /** Return a TSL node that displaces local vertices using the same packed values as `sample`. */
1617
+ displacementNode(timeNode?: three_webgpu.UniformNode<"float", number>): Node<"vec3">;
1618
+ }
1619
+
1620
+ /** How the mirrored pass is sized. Both numbers are cost, not appearance. */
1621
+ interface IWaterReflectionOptions {
1622
+ /**
1623
+ * The mirrored pass's render target, as a fraction of the drawing buffer.
1624
+ *
1625
+ * A reflection is a second draw of the whole world, so this is the one number that decides
1626
+ * whether a water surface is affordable. Half is the usual answer.
1627
+ */
1628
+ readonly resolutionScale: number;
1629
+ /** Whether this surface may appear in other reflectors' passes. Off is one pass; on is n². */
1630
+ readonly bounces?: boolean;
1631
+ }
1632
+ interface IWaterSurfaceOptions {
1633
+ /** World-space height of the surface, in metres. The mirror plane, and where thickness is 0. */
1634
+ readonly level: number;
1635
+ /**
1636
+ * Thickness readings saturate here, in metres.
1637
+ *
1638
+ * A required number because it is the range of the instrument, not a taste: sky behind the
1639
+ * surface has no depth at all, and something has to be reported for it.
1640
+ */
1641
+ readonly maxThickness: number;
1642
+ /** Omit for a surface that reflects nothing; `reflectionAt` then throws rather than lying. */
1643
+ readonly reflection?: IWaterReflectionOptions;
1644
+ }
1645
+ /**
1646
+ * What a horizontal water surface can see: the world mirrored in it, the world beneath it, and
1647
+ * how much water stands between the two.
1648
+ *
1649
+ * This is the render plumbing a water material needs and cannot write portably — a mirrored
1650
+ * camera and its render target, the frame's own colour read back as the light coming up through
1651
+ * the surface, and the scene's depth turned into **metres of water under this pixel**. It decides
1652
+ * nothing about how any of that looks: no colour, no absorption tint, no fresnel weighting, no
1653
+ * glint. Those are the game's, composed from the nodes below in its own `src/render/` material.
1654
+ *
1655
+ * The depth reading is the part worth having. `linearDepth` answers in a normalised 0..1 that
1656
+ * changes meaning with every camera near/far pair, so a material that subtracts two of them gets
1657
+ * a number in no unit at all, and its shoreline moves when the camera's far plane does.
1658
+ * `thicknessAt` returns metres, and metres survive the camera changing.
1659
+ *
1660
+ * ```js
1661
+ * const surface = new WaterSurface3D({ level: 0, maxThickness: 4, reflection: { resolutionScale: 0.5 } });
1662
+ * const offset = normal.xz.mul(0.02);
1663
+ * material.colorNode = mix(
1664
+ * surface.refractionAt(offset).mul(siltTint), // the game's colours,
1665
+ * surface.reflectionAt(offset), // composed by the game,
1666
+ * fresnel, // with the game's own fresnel.
1667
+ * );
1668
+ * material.opacityNode = surface.thicknessAt().div(surface.maxThickness);
1669
+ * ```
1670
+ */
1671
+ declare class WaterSurface3D {
1672
+ #private;
1673
+ readonly maxThickness: number;
1674
+ /**
1675
+ * The object whose plane is mirrored, kept outside the scene graph on purpose.
1676
+ *
1677
+ * A water level is a fact about the world, not about the mesh that happens to draw it: parent
1678
+ * the mirror to the surface mesh, as three's own examples do, and a non-uniform scale anywhere
1679
+ * up that mesh's ancestry skews the plane the reflection is taken about. This one is placed
1680
+ * from `level` and nothing else, so it stays level.
1681
+ */
1682
+ readonly target: Object3D | undefined;
1683
+ constructor(options: IWaterSurfaceOptions);
1684
+ /** The world-space height of the surface, in metres. */
1685
+ get level(): number;
1686
+ /** Move the surface — a tide, a sluice, a flooding room. The mirror plane follows. */
1687
+ setLevel(value: number): void;
1688
+ get released(): boolean;
1689
+ /**
1690
+ * The world mirrored in the surface, at an optional screen-space offset.
1691
+ *
1692
+ * The offset is where a game spends its surface normal: a still mirror takes none, and ripples
1693
+ * are the normal's horizontal part scaled by however far the game wants the reflection to slide.
1694
+ */
1695
+ reflectionAt(offset?: Node<"vec2">): Node<"vec3">;
1696
+ /**
1697
+ * The frame beneath the surface — everything already drawn this frame, read at an offset.
1698
+ *
1699
+ * An offset that lands on something **in front of** the water is refused and the fragment falls
1700
+ * back to a straight read. Without that, a rock standing in the shallows smears across the
1701
+ * water in front of it: the classic refraction bleed, and the reason a hand-rolled offset looks
1702
+ * wrong the first time every game writes one.
1703
+ */
1704
+ refractionAt(offset?: Node<"vec2">): Node<"vec3">;
1705
+ /**
1706
+ * Metres of water between this fragment and whatever is drawn behind it, clamped to
1707
+ * `maxThickness`. Zero exactly where the bed meets the surface, which is the shoreline.
1708
+ *
1709
+ * Sky behind the surface has no depth and reads as `maxThickness`, not as zero: an unbounded
1710
+ * horizon is deep water, not dry land.
1711
+ */
1712
+ thicknessAt(offset?: Node<"vec2">): Node<"float">;
1713
+ /** Drop the mirrored pass and its render target. */
1714
+ dispose(): void;
1715
+ }
1716
+
1717
+ /**
1718
+ * A soft round sprite, built as pixel data rather than by painting a canvas.
1719
+ *
1720
+ * Canvas-drawn images sample black under `WebGPURenderer` — a documented trap that cost a shipped
1721
+ * game real debugging time — so sprite images are written straight into pixel data. A radial alpha
1722
+ * falloff is also the whole difference between a puff and a rectangle: a flat quad reads as a grey
1723
+ * box, the same quad with this alpha reads as smoke.
1724
+ *
1725
+ * @param size edge length in pixels
1726
+ * @param hardness 0 fades from the very centre, 1 keeps a solid core out to the rim
1727
+ */
1728
+ declare function softCircleDataTexture(size?: number, hardness?: number): DataTexture;
1729
+
1730
+ interface ITracerPool3DOptions {
1731
+ /** Slots in the pool. Shots over the count recycle the oldest streak. Default 12. */
1732
+ readonly count?: number;
1733
+ /**
1734
+ * The streak's shape, supplied by the game. The default is a neutral unit-length cylinder
1735
+ * along +Y with its base at the origin — the pool stretches it along y, so any geometry laid
1736
+ * out the same way works. Override it to change the streak's cross-section or silhouette.
1737
+ */
1738
+ readonly geometry?: BufferGeometry;
1739
+ /**
1740
+ * The streak's surface, supplied by the game and cloned per slot so each can fade
1741
+ * independently. Required: pooling, travel and fading are the engine's; what the streak
1742
+ * looks like never is. Set `opacity` to the peak brightness and pass a transparent,
1743
+ * additive surface for the usual bright-fade look.
1744
+ */
1745
+ readonly material: Material;
1746
+ /** Longest streak in metres; shorter shots get a shorter streak. Default 3.2. */
1747
+ readonly segmentLength?: number;
1748
+ /** Travel speed in metres per second. Default 360. */
1749
+ readonly speed?: number;
1750
+ /** Seconds a streak lives before fading out fully. Default 0.11. */
1751
+ readonly lifetime?: number;
1752
+ }
1753
+ /**
1754
+ * Per-shot overrides a game passes to {@link TracerPool3D.spawn} — shot-to-shot variation
1755
+ * keeps two rounds from reading as one drawn line. The values are the game's (usually from
1756
+ * its seeded random so replays stay identical); the pool only applies them.
1757
+ */
1758
+ interface ITracerSpawnOptions {
1759
+ /** Longest streak for this shot, in metres. Defaults to the pool's `segmentLength`. */
1760
+ readonly segmentLength?: number;
1761
+ /** Seconds this streak lives before fading out fully. Defaults to the pool's `lifetime`. */
1762
+ readonly lifetime?: number;
1763
+ /** Multiplier on this streak's cross-section (x/z scale). Default 1. */
1764
+ readonly widthScale?: number;
1765
+ }
1766
+ /**
1767
+ * Pooled travelling bullet streaks for hitscan shots.
1768
+ *
1769
+ * A hitscan round leaves nothing to see, so a shot is only a sound and a number — you cannot tell
1770
+ * where it went or who is firing. The pool draws a short bright segment that travels from the
1771
+ * muzzle toward the point reached and fades out. Segments are stretched cylinders rather than
1772
+ * `Line`s, because line width is not portable across backends and a one-pixel line is invisible at
1773
+ * thirty metres.
1774
+ *
1775
+ * Every member starts visible at zero opacity, so the whole pool doubles as a pipeline prewarm
1776
+ * surface (`prewarm(tracers)`); nothing is created while firing. Call {@link update} once per
1777
+ * frame and {@link dispose} with the owning scene.
1778
+ */
1779
+ declare class TracerPool3D {
1780
+ #private;
1781
+ constructor(parent: Object3D, options: ITracerPool3DOptions);
1782
+ /**
1783
+ * Stop submitting the slots that are not carrying a shot.
1784
+ *
1785
+ * The pool is resident from construction at zero opacity so its pipeline compiles during
1786
+ * loading; the cost is a draw per dead streak every frame forever, and on a phone the draw call
1787
+ * is the expensive part, not the triangles. Once compiled the pipeline is cached, so `spawn`
1788
+ * re-showing a slot is free. Call this a second or two into the scene, after `prewarm` — not on
1789
+ * the first frame, or the compile this exists to force will not have happened yet.
1790
+ */
1791
+ settle(): void;
1792
+ /**
1793
+ * Draw one round travelling from `from` along `direction` for `distance` metres.
1794
+ * `options` carries the game's per-shot variation; omit it for the pool defaults.
1795
+ */
1796
+ spawn(from: Vector3, direction: Vector3, distance: number, options?: ITracerSpawnOptions): void;
1797
+ /** Advance every live streak; call once per frame with the frame's delta seconds. */
1798
+ update(dt: number): void;
1799
+ /** Remove every mesh from the parent and release pooled surfaces. Game-owned geometry survives. */
1800
+ dispose(): void;
1801
+ }
1802
+
93
1803
  interface IPathFollow3DOptions {
94
1804
  readonly loop?: boolean;
95
1805
  readonly points: readonly Vector3[];
96
1806
  readonly speed?: number;
97
1807
  }
98
1808
  interface IPathFollow3DSample {
99
- readonly point: Vector3;
100
- readonly progress: number;
101
- readonly tangent: Vector3;
1809
+ point: Vector3;
1810
+ progress: number;
1811
+ tangent: Vector3;
102
1812
  }
103
1813
  interface IPathFollow3DProjection {
104
- readonly distanceFromStart: number;
105
- readonly lateralDistance: number;
106
- readonly tangent: Vector3;
107
- readonly point: Vector3;
108
- readonly segment: number;
1814
+ distanceFromStart: number;
1815
+ lateralDistance: number;
1816
+ tangent: Vector3;
1817
+ point: Vector3;
1818
+ segment: number;
109
1819
  }
110
1820
  /** A portable, distance-based follower for an authored Three.js route. */
111
1821
  declare class PathFollow3D {
@@ -118,24 +1828,367 @@ declare class PathFollow3D {
118
1828
  get progress(): number;
119
1829
  get speed(): number;
120
1830
  set speed(value: number);
121
- advance(dt: number): IPathFollow3DSample;
1831
+ advance(dt: number, target?: IPathFollow3DSample): IPathFollow3DSample;
122
1832
  progressTo(distance: number): this;
123
- sample(distance?: number): IPathFollow3DSample;
124
- pointAt(distance: number): IPathFollow3DSample;
125
- project(position: Vector3): IPathFollow3DProjection;
1833
+ sample(distance?: number, target?: IPathFollow3DSample): IPathFollow3DSample;
1834
+ pointAt(distance: number, target?: IPathFollow3DSample): IPathFollow3DSample;
1835
+ project(position: Vector3, target?: IPathFollow3DProjection): IPathFollow3DProjection;
1836
+ }
1837
+
1838
+ interface IGroundSnapOptions {
1839
+ /** Whether to apply the correction. Measurement and `clearance` continue when this is false. */
1840
+ readonly enabled?: boolean;
1841
+ /** Maximum correction speed in metres per second. Unset follows the authored pose exactly. */
1842
+ readonly maxRate?: number;
1843
+ /** Visual meshes to measure. Defaults to every mesh below `model`. */
1844
+ readonly meshes?: readonly Object3D[];
126
1845
  }
1846
+ /**
1847
+ * Keeps the lowest posed point of a rendered model on a surface.
1848
+ *
1849
+ * This is render grounding, not collider snap-to-ground. It uses a cached skin envelope so a
1850
+ * frame loop never runs the precise per-vertex bounds path. `enabled` is deliberately a range:
1851
+ * turning correction off still leaves `clearance` and `audit()` truthful.
1852
+ */
1853
+ declare class GroundSnap {
1854
+ readonly model: Object3D;
1855
+ readonly meshes: readonly Object3D[] | undefined;
1856
+ enabled: boolean;
1857
+ maxRate: number | undefined;
1858
+ clearance: number | null;
1859
+ private readonly parentInverse;
1860
+ private readonly parentOrigin;
1861
+ private readonly parentTarget;
1862
+ constructor(model: Object3D, options?: IGroundSnapOptions);
1863
+ /** Move `group` so its lowest posed point meets `surfaceY`, then report the real clearance. */
1864
+ apply(group: Object3D, surfaceY: number, dt: number): void;
1865
+ private applyWorldCorrection;
1866
+ /**
1867
+ * Compare the cheap envelope's lower bound with a precise vertex measurement.
1868
+ *
1869
+ * This is intentionally opt-in: calling it in `apply()` would restore the frame-time defect
1870
+ * this class exists to remove. A negative result means the envelope is below the precise skin.
1871
+ */
1872
+ audit(): number | null;
1873
+ }
1874
+
1875
+ type ThreePoseVector = readonly [number, number, number];
1876
+ type ThreePoseQuaternion = readonly [number, number, number, number];
1877
+ interface IThreePoseBounds {
1878
+ readonly min: ThreePoseVector;
1879
+ readonly max: ThreePoseVector;
1880
+ readonly size: ThreePoseVector;
1881
+ }
1882
+ /** JSON-safe world-space measurements for one Three.js object and its visual bounds. */
1883
+ interface IThreePoseMeasurement {
1884
+ readonly name: string;
1885
+ readonly type: string;
1886
+ readonly position: ThreePoseVector;
1887
+ readonly quaternion: ThreePoseQuaternion;
1888
+ readonly scale: ThreePoseVector;
1889
+ readonly axes: {
1890
+ readonly x: ThreePoseVector;
1891
+ readonly y: ThreePoseVector;
1892
+ readonly z: ThreePoseVector;
1893
+ };
1894
+ readonly bounds: IThreePoseBounds | null;
1895
+ }
1896
+ interface IMeasureThreePoseOptions {
1897
+ /**
1898
+ * Objects whose geometry forms the reported bounds. Defaults to `object`.
1899
+ * This path is precise; it walks every vertex. Do not call it in a frame loop — see
1900
+ * `posedBounds`.
1901
+ */
1902
+ readonly bounds?: readonly Object3D[] | false;
1903
+ }
1904
+ /**
1905
+ * Measure an Object3D in world space for attachment and animation diagnostics.
1906
+ *
1907
+ * Passing explicit `bounds` lets a probe measure a body without an attached weapon or
1908
+ * invisible hitbox. The result is JSON-safe so it can cross the browser playtest bridge.
1909
+ */
1910
+ declare function measureThreePose(object: Object3D, options?: IMeasureThreePoseOptions): IThreePoseMeasurement;
1911
+ /**
1912
+ * Cheap world-space bounds for a posed model.
1913
+ *
1914
+ * The first call pays the precise vertex walk to build a conservative skin envelope. Later calls
1915
+ * read one world-matrix translation per contributing bone and allocate nothing. The returned
1916
+ * object is cached for the root and updated in place; copy it if it must outlive the next call.
1917
+ */
1918
+ declare function posedBounds(root: Object3D, meshes?: readonly Object3D[]): IThreePoseBounds;
1919
+
1920
+ type PlatformRuntime = "web" | "native";
1921
+ type PlatformOS = "android" | "ios" | "linux" | "macos" | "windows" | "unknown";
1922
+ type PlatformFormFactor = "mobile" | "desktop" | "unknown";
1923
+ interface IPlatformInfo {
1924
+ readonly runtime: PlatformRuntime;
1925
+ readonly os: PlatformOS;
1926
+ readonly formFactor: PlatformFormFactor;
1927
+ readonly maxTouchPoints: number;
1928
+ }
1929
+ declare function getPlatform(): Readonly<IPlatformInfo>;
1930
+ declare function isWeb(): boolean;
1931
+ declare function isNative(): boolean;
1932
+ declare function isMobile(): boolean;
1933
+ declare function isTouchscreenAvailable(): boolean;
1934
+
1935
+ type ReplayPointer = readonly [number, number, number, number, number];
1936
+ interface IReplayRecordingSample {
1937
+ readonly keys: readonly string[];
1938
+ readonly pointer?: ReplayPointer;
1939
+ readonly tick: number;
1940
+ }
1941
+ interface IReplayRecording {
1942
+ readonly input: readonly IReplayRecordingSample[];
1943
+ readonly randomState: number;
1944
+ readonly runtime: {
1945
+ agent: string;
1946
+ core: string;
1947
+ portable?: boolean;
1948
+ rapier: string | null;
1949
+ step: number;
1950
+ };
1951
+ readonly seed: number;
1952
+ readonly ticks: number;
1953
+ readonly version: 1;
1954
+ }
1955
+ declare function parseReplayRecording(value: unknown): IReplayRecording;
127
1956
 
128
1957
  type Recording = IReplayRecording;
1958
+ interface IReplayOptions {
1959
+ /** Allow a recording to be replayed by a different host with the same simulation contract. */
1960
+ readonly portable?: boolean;
1961
+ }
129
1962
  type ReplayPublic = {
130
1963
  readonly recording: Recording | undefined;
131
1964
  readonly runId: symbol;
132
1965
  };
133
- declare function replay<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined>(): IGamePluginHooks<TState, TPhysics> & ReplayPublic;
1966
+ declare function replay<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined>(options?: IReplayOptions): IGamePluginHooks<TState, TPhysics> & ReplayPublic;
134
1967
  declare function createReplayDriver(recording: Recording, target: EventTarget, pointerTarget?: EventTarget): ((runtime: IGamePluginRuntime) => number) & {
135
1968
  prepare: (runtime: IGamePluginRuntime) => void;
136
1969
  runId: symbol;
137
1970
  };
138
1971
 
139
- declare const version = "0.1.0";
1972
+ type SpritePlaybackMode = "loop" | "pingPong" | "once";
1973
+ interface ISpriteFrame3D {
1974
+ /** Pixels from the left edge of the atlas. */
1975
+ readonly x: number;
1976
+ /** Pixels from the top edge when `origin` is `"top-left"`. */
1977
+ readonly y: number;
1978
+ readonly width: number;
1979
+ readonly height: number;
1980
+ /** Seconds this frame is held; every frame must provide its own authored timing. */
1981
+ readonly duration: number;
1982
+ }
1983
+ interface ISpriteAnimator3DOptions {
1984
+ /** The game-owned atlas texture. Its filters, wrapping and surface remain untouched. */
1985
+ readonly texture: Texture;
1986
+ /** Pixel-space atlas rectangles with per-frame durations. */
1987
+ readonly frames: readonly ISpriteFrame3D[];
1988
+ readonly mode?: SpritePlaybackMode;
1989
+ /** Atlas coordinate origin; top-left is conventional for exported sprite sheets. */
1990
+ readonly origin?: "top-left" | "bottom-left";
1991
+ /** Start advancing immediately unless the game explicitly opts out. */
1992
+ readonly autoPlay?: boolean;
1993
+ }
1994
+ /**
1995
+ * Advance a game-owned atlas texture on the fixed step supplied by its owner.
1996
+ *
1997
+ * This helper changes only `texture.offset` and `texture.repeat`. The game still chooses the
1998
+ * texture, filters, wrapping, surface, geometry and every frame's duration. Call {@link update}
1999
+ * from the scene's fixed-step update; no wall clock or global animation loop is consulted.
2000
+ */
2001
+ declare class SpriteAnimator3D {
2002
+ #private;
2003
+ readonly texture: Texture;
2004
+ readonly frames: readonly ISpriteFrame3D[];
2005
+ readonly mode: SpritePlaybackMode;
2006
+ readonly origin: "top-left" | "bottom-left";
2007
+ constructor(options: ISpriteAnimator3DOptions);
2008
+ get frameIndex(): number;
2009
+ get elapsed(): number;
2010
+ get finished(): boolean;
2011
+ get playing(): boolean;
2012
+ /** Advance by one caller-supplied fixed-step delta. */
2013
+ update(dt: number): this;
2014
+ /** Pause fixed-step advancement while leaving the selected frame visible. */
2015
+ pause(): this;
2016
+ /** Resume, restarting a completed one-shot from frame zero. */
2017
+ play(): this;
2018
+ /** Stop and reset to the first authored frame. */
2019
+ stop(): this;
2020
+ /** Select a frame without advancing time. */
2021
+ setFrame(index: number): this;
2022
+ }
2023
+
2024
+ /** Names of every bone in traversal order. Empty for an unskinned model. */
2025
+ declare function skeletonBones(root: Object3D): readonly string[];
2026
+ /** Parent a child to a named bone while preserving the child's authored world scale. */
2027
+ declare function attachToBone(root: Object3D, boneName: string, child: Object3D): Object3D;
2028
+ /** How far a bone sits from the object it is supposed to be touching. */
2029
+ interface IBoneContactReport {
2030
+ readonly bone: string;
2031
+ readonly target: string;
2032
+ /** Metres from the bone to the nearest point of the target's world bounds. Zero when inside. */
2033
+ readonly distance: number;
2034
+ readonly inside: boolean;
2035
+ readonly bonePosition: ThreePoseVector;
2036
+ readonly targetPoint: ThreePoseVector;
2037
+ }
2038
+ /**
2039
+ * Measure whether a named bone reaches a game object, in metres.
2040
+ *
2041
+ * This turns "his hands are on the keyboard" and "he is sitting on the chair" into numbers a
2042
+ * scenario can assert. It walks the target's vertices through `measureThreePose`, so call it on
2043
+ * a check or a debug sample rather than every frame.
2044
+ */
2045
+ declare function boneContact(root: Object3D, boneName: string, target: Object3D): IBoneContactReport;
2046
+
2047
+ /** One track of a clip, and whether it resolves to a real property on a root. */
2048
+ interface IClipTrackBinding {
2049
+ readonly track: string;
2050
+ /** The node the track names, or `null` when nothing under the root carries that name. */
2051
+ readonly node: string | null;
2052
+ readonly bound: boolean;
2053
+ /** Three.js's own reason the track binds nothing. `null` when it binds. */
2054
+ readonly reason: string | null;
2055
+ }
2056
+ interface IClipBindingReport {
2057
+ readonly clip: string;
2058
+ readonly tracks: number;
2059
+ readonly bound: number;
2060
+ /** Every track that drives nothing, in clip order. */
2061
+ readonly unbound: readonly IClipTrackBinding[];
2062
+ }
2063
+ interface IClipCoverageReport {
2064
+ readonly clip: string;
2065
+ readonly bones: number;
2066
+ readonly driven: readonly string[];
2067
+ /** Bones no bound track touches. They hold whatever the previous clip left them in. */
2068
+ readonly undriven: readonly string[];
2069
+ }
2070
+ interface IClipPoseSubject {
2071
+ readonly root: Object3D;
2072
+ readonly clip: AnimationClip;
2073
+ }
2074
+ interface IClipPoseErrorOptions {
2075
+ /** Measured bone name to reference bone name. Defaults to the names the two rigs share. */
2076
+ readonly bones?: Readonly<Record<string, string>>;
2077
+ /** Poses compared across the clip. Defaults to 8. */
2078
+ readonly samples?: number;
2079
+ }
2080
+ interface IBonePoseError {
2081
+ readonly bone: string;
2082
+ readonly reference: string;
2083
+ readonly meanDegrees: number;
2084
+ readonly maxDegrees: number;
2085
+ readonly maxAtSeconds: number;
2086
+ }
2087
+ interface IClipPoseErrorReport {
2088
+ readonly clip: string;
2089
+ readonly referenceClip: string;
2090
+ readonly samples: number;
2091
+ readonly meanDegrees: number;
2092
+ readonly maxDegrees: number;
2093
+ /** Every compared bone, worst mean first. */
2094
+ readonly bones: readonly IBonePoseError[];
2095
+ }
2096
+ /**
2097
+ * Report which of a clip's tracks resolve to a real property on `root`.
2098
+ *
2099
+ * A retarget that writes the wrong glTF target path produces tracks named `<bone>.undefined`:
2100
+ * they load without error, bind nothing, and the character plays its bind pose.
2101
+ */
2102
+ declare function clipTrackBindings(root: Object3D, clip: AnimationClip): IClipBindingReport;
2103
+ /**
2104
+ * Report which bones of `root` a clip does not drive.
2105
+ *
2106
+ * An undriven bone keeps whatever the previous clip left it in, so a rig whose clips cover 22 of
2107
+ * 65 bones carries the last walk cycle's hand shape into every pose that follows.
2108
+ */
2109
+ declare function clipBoneCoverage(root: Object3D, clip: AnimationClip): IClipCoverageReport;
2110
+ /**
2111
+ * Score how far a clip's pose is from the same pose on a reference rig, in degrees.
2112
+ *
2113
+ * Each bone is compared as its world rotation *relative to its own rig's bind pose*, as a whole
2114
+ * quaternion. The delta makes the two rigs' bind conventions cancel, so rigs whose arms sit 90
2115
+ * degrees apart at rest still score zero when the retarget is right; the whole quaternion makes
2116
+ * twist count, which a bone-direction check cannot see — a forearm rolled about its own axis
2117
+ * points exactly where it should while the skin between elbow and wrist tears into a smear.
2118
+ *
2119
+ * Both rigs are driven and then restored to the transforms they arrived with.
2120
+ */
2121
+ declare function clipPoseError(subject: IClipPoseSubject, reference: IClipPoseSubject, options?: IClipPoseErrorOptions): IClipPoseErrorReport;
2122
+
2123
+ /**
2124
+ * One rig's parent→child bone distances, captured at one moment.
2125
+ *
2126
+ * A rigid skeleton preserves every parent→child distance under any pose — that is what makes
2127
+ * bone length the one number that names a broken pose without a screenshot (PRD-324).
2128
+ */
2129
+ interface IBoneLengthSnapshot {
2130
+ readonly bones: number;
2131
+ /** Bone name → world distance to its parent bone, in metres. Bones without a bone parent are absent. */
2132
+ readonly lengths: Readonly<Record<string, number>>;
2133
+ }
2134
+ /** One bone whose parent→child distance changed between the snapshot and now. */
2135
+ interface IBoneLengthDeviation {
2136
+ readonly bone: string;
2137
+ /** The distance at the snapshot, in metres. */
2138
+ readonly bindLength: number;
2139
+ /** The distance now, in metres. */
2140
+ readonly posedLength: number;
2141
+ /** `posedLength / bindLength`; `Infinity` when the bind distance was zero. */
2142
+ readonly ratio: number;
2143
+ /** `|posedLength − bindLength|`, in metres. */
2144
+ readonly delta: number;
2145
+ }
2146
+ interface IBoneLengthDeviationReport {
2147
+ readonly bones: number;
2148
+ /** Bones actually compared: named in both the snapshot and now. */
2149
+ readonly compared: number;
2150
+ /** The largest relative change across compared bones. Zero for a rigid pose. */
2151
+ readonly maxDeviation: number;
2152
+ /** The worst deviating bone, or `null` when the pose is rigid. */
2153
+ readonly worst: IBoneLengthDeviation | null;
2154
+ /** Every bone past `tolerance`, worst first. */
2155
+ readonly deviations: readonly IBoneLengthDeviation[];
2156
+ /** True when no bone moved past `tolerance` relative to its bind distance. */
2157
+ readonly rigid: boolean;
2158
+ }
2159
+ interface IBoneLengthDeviationsOptions {
2160
+ /**
2161
+ * Allowed relative change per bone before it is named. Default `0.01` — one per cent of its
2162
+ * own bind length, which float noise sits far below and a pose defect sits far above.
2163
+ */
2164
+ readonly tolerance?: number;
2165
+ }
2166
+ /**
2167
+ * Measure every parent→child bone distance, in world space, as the rig stands right now.
2168
+ *
2169
+ * World distances, so a uniform ancestor scale is part of the number: capture the baseline and
2170
+ * the comparison under the same ancestor transform (in the usual shape, both after the rig's
2171
+ * normalisation) and the scale cancels in the comparison.
2172
+ */
2173
+ declare function boneLengths(root: Object3D): IBoneLengthSnapshot;
2174
+ /**
2175
+ * Compare a rig's parent→child bone distances now against a captured snapshot.
2176
+ *
2177
+ * A rigid skeleton preserves every parent→child distance under any pose, so any named bone is
2178
+ * a defect with an address: something moved a bone away from its parent — a position or scale
2179
+ * track in the wrong space, a bind mismatch, a clipped hierarchy — and no screenshot needs to
2180
+ * be squinted at to say which one (PRD-324).
2181
+ */
2182
+ declare function boneLengthDeviations(root: Object3D, bind: IBoneLengthSnapshot, options?: IBoneLengthDeviationsOptions): IBoneLengthDeviationReport;
2183
+
2184
+ /**
2185
+ * The version this library reports.
2186
+ *
2187
+ * It read `0.1.0` while the package published `0.2.0`, and `__tests__/build.spec.ts` asserted the
2188
+ * stale literal, so the test held the bug in place rather than catching it. A literal is
2189
+ * unavoidable here — core is bundled for browsers and cannot read `package.json` at runtime — so
2190
+ * the spec now asserts this equals the manifest instead of asserting a number somebody typed.
2191
+ */
2192
+ declare const version = "0.3.1";
140
2193
 
141
- export { AnimationPlayer, GPUParticles3D, IGamePluginHooks, IGamePluginRuntime, type IPathFollow3DOptions, type IPathFollow3DProjection, type IPathFollow3DSample, type IThreeNativeConfig, PathFollow3D, type Recording, type ThreeNativeOrientation, createReplayDriver, replay, version };
2194
+ export { ATLAS_PADDING, ATMOSPHERE_LUT_RESOLUTIONS, AnimationPlayer, Atmosphere, type AtmosphereDirection, AtmosphereLuts, type AtmosphereRgb, Billboard3D, type BillboardLockAxis, CameraShake, type CameraShakeCurve, ClusteredBatch, ClusteredMesh, FluidField2D, GPUParticles3D, GPUSceneBVH, type GPUSceneBVHTraceFunction, GroundSnap, type IAddInSlicesOptions, type IAddInSlicesProgress, type IAddInSlicesReport, type IAnimationPlayOptions, type IAnimationPlayerOptions, type IAtmosphereLutResolution, type IAtmosphereLutResolutions, type IAtmosphereOptions, type IAtmosphereParameterPatch, type IAtmosphereParameters, type IAtmosphereScenePass, type IBillboard3DOptions, type IBoneContactReport, type IBoneLengthDeviation, type IBoneLengthDeviationReport, type IBoneLengthDeviationsOptions, type IBoneLengthSnapshot, type IBonePoseError, type ICameraShakeOffset, type ICameraShakeOptions, type IClipBindingReport, type IClipCoverageReport, type IClipPoseErrorOptions, type IClipPoseErrorReport, type IClipPoseSubject, type IClipTrackBinding, type IClusterTable, type IClusteredBatchBuildOptions, type IClusteredBatchOptions, type IClusteredMeshOptions, type IClusteredPlacement, IComputeDriven$1 as IComputeDriven, type IFluidFieldOptions, type IFluidFieldSampler, type IFluidFieldVector2, IGPUReadbackSample, type IGPUSceneBVHMaterialGroup, type IGPUSceneBVHOptions, IGamePluginHooks, IGamePluginRuntime, type IGroundSnapOptions, type IInstancedBatchBuildOptions, type IInstancedBatchOptions, type IInstancedPlacement, type ILoadAllOptions, type ILoadAllProgress, type IMeasureThreePoseOptions, type IMergePart, type IMergePartsOptions, type INormaliseToMetresOptions, type IPathFollow3DOptions, type IPathFollow3DProjection, type IPathFollow3DSample, type IPlatformInfo, type IProbeVolumeBakeProgress, type IProbeVolumeCoefficient, type IProbeVolumeObservation, type IProbeVolumeOptions, type IReplayOptions, type IReplayRecording, type IReplayRecordingSample, type IResolvedAtmosphereParameters, type ISkeletalMesh3DOptions, type ISoftBody3DOptions, type ISoftBodyCollision, type ISolarPosition, type ISolarPositionInput, type ISpectralOceanCascade, type ISpectralOceanHeight, type ISpectralOceanOptions, type ISpriteAnimator3DOptions, type ISpriteFrame3D, type IStrideReport, type IThreePoseBounds, type IThreePoseMeasurement, type ITracerPool3DOptions, type ITracerSpawnOptions, type IVirtualShadowOptions, type IVirtualShadowStats, type IWaterReflectionOptions, type IWaterSurfaceOptions, type IWaveFieldDomainWarp, type IWaveFieldGraphOptions, type IWaveFieldOptions, type IWaveFieldSample, type IWaveFieldWave, InstancedBatch, LUT_RESOLUTIONS, type NormaliseAxis, PROBE_VOLUME_MARKER, PathFollow3D, type PlatformFormFactor, type PlatformOS, type PlatformRuntime, ProbeVolume, type ProbeVolumeDensity, type Recording, type ReplayPointer, SkeletalMesh3D, SoftBody3D, SpectralOcean, SpriteAnimator3D, type SpritePlaybackMode, type ThreePoseQuaternion, type ThreePoseVector, TracerPool3D, VIRTUAL_SHADOW_MARKER, VIRTUAL_SHADOW_MOVER_LAYER, VirtualShadowNode, WaterSurface3D, type WaveDirection, WaveField, addInSlices, attachToBone, boneContact, boneLengthDeviations, boneLengths, bvhIntersectFirstHit, clipBoneCoverage, clipPoseError, clipTrackBindings, createReplayDriver, directionFromSolarPosition, directionalTransmittance, getPlatform, isMobile, isNative, isTouchscreenAvailable, isWeb, loadAll, measureThreePose, mergeParts, normaliseToMetres, parseReplayRecording, posedBounds, rayStruct, readProbeVolumeObservation, readVirtualShadowMarker, replay, resolveAtmosphereLutResolutions, resolveAtmosphereParameters, skeletonBones, softCircleDataTexture, solarPosition, solarPositionAt, updateAtmosphereParameters, updateClusteredMeshes, version, zenithTransmittance };