@threenative/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,85 +1,1524 @@
1
- export { A as AudioBus, b as AudioBusOptions, c as AudioPlayOptions } from './audio-3vkjtiuo.js';
2
- import { R as RendererLike, a as GamePluginRuntime, b as GamePluginHooks } from './game-DRPs3M7r.js';
3
- export { A as AssetLoader, c as AssetLoaderOptions, C as CameraConfig, d as Ctx, D as Debuggable, E as EntitySnapshot, G as Game, e as GameConfig, f as GamePlatformSource, g as GamePlugin, h as GamePluginFunction, i as GameStore, I as InputAction, j as InputBindings, k as InputMap, O as OrthogonalCameraConfig, P as PerspectiveCameraConfig, l as PluginCleanup, m as Random, n as RawInputState, o as Registry, p as RendererKind, q as RendererOptions, S as Scene, r as SceneConstructor, s as SceneEnterResult, t as SceneFrame, u as ScheduleHandle, v as Scheduler, w as StatePatch, V as Viewport, x as ViewportOptions, y as ViewportResizeHandler, z as ViewportSize, B as autoFields, F as createAssetLoader, H as createGameStore, J as createRandom, K as createRenderer, L as defineGame, M as input } from './game-DRPs3M7r.js';
4
- import { AnimationMixer, AnimationClip, Object3D, Sprite } from 'three';
5
- import { StorageBufferNode, SpriteNodeMaterial, ComputeNode } from 'three/webgpu';
6
- import { IReplayRecording } from '@threenative/playtest';
1
+ import * as three from 'three';
2
+ import { AnimationMixer, AnimationClip, Object3D, Camera, Vector3, Group, Matrix4, Mesh, BufferGeometry, Material, InstancedMesh, Box3, Scene, Sprite, DataTexture, CatmullRomCurve3, Texture } from 'three';
3
+ import { a as IGamePluginRuntime, b as IGamePluginHooks } from './game-CYIaKhgl.js';
4
+ export { C as ContextMenuPolicy, c as IAssetLoader, d as IAssetLoaderOptions, e as ICtx, I as IGame, f as IGameObservationContribution, g as IGameObservationSampleRequest, h as IGamePlatformSource, i as IInputAction, j as IInputGamepad, k as IPointerDragHandle, l as IPointerEvent3D, m as IPointerEvents3D, n as IPointerEvents3DOptions, o as IPointerEvents3DPicker, p as IPointerState, q as IRandom, r as IRawInputPointer, s as IRawInputPointerEdge, t as IRawInputState, u as IRaycastOptions, v as IScenePickerOptions, w as IThreeNativeBootSplash, x as IThreeNativeConfig, y as IThreeNativeIconVariants, z as IThreeNativeTexturesConfig, A as ITweenOptions, B as IWarmUpOptions, D as IWarmUpProgress, E as IWarmUpRenderer, F as IWarmUpReport, G as InputBindings, H as InputPlatformSource, P as PointerEvent3DListener, J as PointerEvent3DType, K as PointerEvents3D, S as Scene, L as SceneFrame, M as ScenePicker, N as ScheduleHandle, O as Scheduler, T as ThreeNativeBackgroundMode, Q as ThreeNativeOrientation, R as ThreeNativeUiRenderer, U as createAssetLoader, V as createRandom, W as defineGame, X as warmUpScene } from './game-CYIaKhgl.js';
5
+ export { A as AudioBus, I as IAudioBusOptions, b as IAudioPlayOptions } from './audio-Dp2mXpD3.js';
6
+ import { I as IRendererLike } from './canvas-layer-CtrZHgIh.js';
7
+ export { C as CanvasLayer, 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 IRenderChainApplied, j as IRenderChainBudgetWindow, k as IRenderChainDroppedStage, l as IRenderChainOptions, m as IRenderChainRenderer, n as IRenderChainRequest, o as IRenderChainStage, p as IRenderChainStageContext, q as IRenderChainVelocityMeasurement, r as IRenderChainVelocityReport, s as IRenderChainVelocityRequest, t as IRenderChainVelocityResult, u as IVelocityRenderPass, R as RENDER_CHAIN_MARKER, v as RENDER_CHAIN_STAGE_ORDER, w as RENDER_CHAIN_TIERS, x as RenderChain, y as RenderChainSource, z as RenderChainStageName, A as RenderChainTier, B as RenderChainTierRequest, D as RenderChainVelocitySource, V as VELOCITY_OUTPUT_NAME, E as VELOCITY_PREVIOUS_BONE_MATRICES, G as VELOCITY_PREVIOUS_INSTANCE_MATRICES, H as VELOCITY_PREVIOUS_WORLD_MATRIX, J as VelocityTracker, K as ensureVelocityOutput, L as prewarm, M as readRenderChainObservation, N as readRenderChainReport, O as readVelocityPreviousBoneMatrices, P as readVelocityPreviousMatrices, Q as readVelocityPreviousWorldMatrix, S as velocityTexture, T as withVelocityContext } from './canvas-layer-CtrZHgIh.js';
8
+ import * as three_webgpu from 'three/webgpu';
9
+ import { StorageTexture, ComputeNode, UniformNode, Node, TextureNode, NodeMaterial, StorageBufferNode, StructTypeNode, Data3DTexture, SpriteNodeMaterial, StorageTextureNode } from 'three/webgpu';
10
+ import { Node as Node$1 } from 'three/src/nodes/Nodes.js';
7
11
  import 'zustand/vanilla';
8
12
 
9
- interface AnimationPlayerOptions {
13
+ interface IAnimationPlayerOptions {
10
14
  readonly clips: readonly AnimationClip[];
11
15
  readonly root: Object3D;
16
+ /**
17
+ * Match a travelling clip's playback rate to the ground the body actually covers.
18
+ *
19
+ * On by default, because a model whose feet do not agree with its motion is the single most
20
+ * common thing wrong with a character in a game built here, and every game solves it the same
21
+ * way. Set `false` to keep the authored rate; the measurement below stays live either way and
22
+ * says that it was overridden. The convention re-times locomotion only: a `"once"` clip — a
23
+ * death, a flinch — always plays at its authored rate.
24
+ */
25
+ readonly strideSync?: boolean;
26
+ /**
27
+ * The object whose travel counts as ground covered. Defaults to `root`.
28
+ *
29
+ * Name the body a game moves when the rig is a child of it, which is the usual shape: the clip
30
+ * writes the model's own root track, so measuring the same object the mixer writes would read
31
+ * the clip's motion back as if it were the body's.
32
+ */
33
+ readonly strideRoot?: Object3D;
12
34
  }
13
- interface AnimationPlayOptions {
35
+ /**
36
+ * What the feet are doing against what the body is doing.
37
+ *
38
+ * Reported whether or not the convention is applied: turning a convention off must not turn its
39
+ * measurement off, or a game that opted out has no way to know what it cost.
40
+ */
41
+ interface IStrideReport {
42
+ /** Metres of ground the current clip carries per clip-second, at rate 1. Zero if it travels none. */
43
+ readonly clipGroundSpeed: number;
44
+ /** Metres per second the root has actually covered, smoothed over the last update. */
45
+ readonly groundSpeed: number;
46
+ /** The playback rate those two imply, clamped to `limits`. */
47
+ readonly rate: number;
48
+ /** True when that rate is being applied to the action. */
49
+ readonly synced: boolean;
50
+ /** True when a rate was measured and deliberately not applied. */
51
+ readonly overridden: boolean;
52
+ }
53
+ interface IAnimationPlayOptions {
14
54
  readonly fade?: number;
55
+ /**
56
+ * `"loop"` (default) repeats; `"once"` plays through and holds the last frame. A `"once"` clip
57
+ * also keeps its authored rate — stride sync re-times locomotion, not events.
58
+ */
59
+ readonly mode?: "loop" | "once";
15
60
  }
16
61
  declare class AnimationPlayer {
17
62
  #private;
18
63
  readonly mixer: AnimationMixer;
19
- constructor(options: AnimationPlayerOptions);
64
+ constructor(options: IAnimationPlayerOptions);
20
65
  get current(): string | undefined;
21
66
  get advancedFrames(): number;
22
- play(name: string, options?: AnimationPlayOptions): void;
67
+ /** True when a `"once"` clip has reached its end and is holding. */
68
+ get finished(): boolean;
69
+ /**
70
+ * What the feet are doing against what the body is doing, as of the last `update`.
71
+ *
72
+ * Live whether or not the convention is applied. A game that set `strideSync: false` reads
73
+ * `overridden: true` here next to the rate it declined, which is the only way an override can
74
+ * be honest about what it turned off.
75
+ */
76
+ get stride(): IStrideReport;
77
+ /** The clip behind a name, for a game that wants the action or the raw `AnimationClip`. */
78
+ clip(name: string): AnimationClip;
79
+ play(name: string, options?: IAnimationPlayOptions): void;
23
80
  update(dt: number): void;
24
81
  stop(): void;
25
82
  dispose(): void;
26
83
  }
27
84
 
28
- interface FixedStepLoopOptions {
29
- readonly step?: number;
30
- readonly maxSteps?: number;
31
- readonly onUpdate: (dt: number) => void;
32
- readonly onRender?: () => void;
33
- readonly requestFrame?: (callback: (time: number) => void) => number;
34
- readonly cancelFrame?: (handle: number) => void;
85
+ type BillboardLockAxis = "x" | "y" | "z";
86
+ interface IBillboard3DOptions {
87
+ /** Camera whose view direction or position the object follows. */
88
+ readonly camera: Camera;
89
+ /** Restrict the facing rotation to one world axis, for example `"y"` for a tree. */
90
+ readonly lockAxis?: BillboardLockAxis;
35
91
  }
36
- declare class FixedStepLoop {
92
+ /**
93
+ * Orient one game-owned object toward a camera without owning its geometry or surface.
94
+ *
95
+ * The object is updated only when its owner calls {@link update}; there is no scene-wide registry
96
+ * or per-frame traversal. Perspective cameras face from the object's world position toward the
97
+ * camera, while orthographic cameras use their parallel view direction. The final world rotation
98
+ * is converted back into the object's local space, so a rotated parent remains correct.
99
+ */
100
+ declare class Billboard3D {
37
101
  #private;
38
- readonly step: number;
39
- readonly maxSteps: number;
40
- constructor(options: FixedStepLoopOptions);
41
- get running(): boolean;
42
- get fps(): number;
43
- start(now?: number): void;
44
- stop(): void;
45
- stepFrame(now: number): number;
46
- advance(ticks: number): number;
102
+ readonly object: Object3D;
103
+ readonly camera: Camera;
104
+ readonly lockAxis: BillboardLockAxis | undefined;
105
+ constructor(object: Object3D, options: IBillboard3DOptions);
106
+ /** Apply the current camera pose to the object and return this helper for fluent setup. */
107
+ update(camera?: Camera): this;
108
+ }
109
+
110
+ type CameraShakeCurve = (phase: number) => number;
111
+ interface ICameraShakeOptions {
112
+ /** Position amplitude in world metres, supplied by the game. */
113
+ readonly amplitude: Vector3;
114
+ /** Rotation amplitude in radians around x/y/z, supplied by the game. */
115
+ readonly rotationAmplitude: Vector3;
116
+ /** Curve cycles per second, supplied by the game. */
117
+ readonly frequency: number;
118
+ /** Exponential envelope decay per second, supplied by the game. */
119
+ readonly decay: number;
120
+ /** Game-authored waveform sampled at `elapsed * frequency * 2π`. */
121
+ readonly curve: CameraShakeCurve;
122
+ }
123
+ interface ICameraShakeOffset {
124
+ readonly position: Vector3;
125
+ readonly rotation: Vector3;
126
+ }
127
+ /**
128
+ * Produce a transient camera offset without owning or mutating a camera.
129
+ *
130
+ * The caller supplies the amplitudes, frequency, decay and waveform. {@link update} evaluates the
131
+ * waveform against the caller's fixed-step delta and returns a reusable position/rotation offset;
132
+ * a template can compose it after its own camera rig and damping.
133
+ */
134
+ declare class CameraShake {
135
+ #private;
136
+ readonly amplitude: Vector3;
137
+ readonly rotationAmplitude: Vector3;
138
+ readonly frequency: number;
139
+ readonly decay: number;
140
+ readonly curve: CameraShakeCurve;
141
+ readonly offset: ICameraShakeOffset;
142
+ constructor(options: ICameraShakeOptions);
143
+ get active(): boolean;
144
+ get elapsed(): number;
145
+ /** Start or restart the authored shake waveform. */
146
+ trigger(): this;
147
+ /** Stop the effect and clear the offset. */
148
+ stop(): this;
149
+ /** Evaluate the current offset and advance by one caller-supplied fixed-step delta. */
150
+ update(dt: number): ICameraShakeOffset;
151
+ }
152
+
153
+ type AtmosphereRgb = readonly [number, number, number];
154
+ type AtmosphereVector = AtmosphereRgb | Readonly<{
155
+ x: number;
156
+ y: number;
157
+ z: number;
158
+ }> | Vector3;
159
+ /** Physical inputs for the atmosphere model. Coefficients use 1/km and radii use km. */
160
+ interface IAtmosphereParameters {
161
+ readonly rayleigh: AtmosphereVector;
162
+ readonly mie: AtmosphereVector;
163
+ readonly ozone: AtmosphereVector;
164
+ readonly planetRadius: number;
165
+ readonly atmosphereRadius: number;
166
+ }
167
+ interface IResolvedAtmosphereParameters {
168
+ readonly rayleigh: Vector3;
169
+ readonly mie: Vector3;
170
+ readonly ozone: Vector3;
171
+ readonly planetRadius: number;
172
+ readonly atmosphereRadius: number;
173
+ }
174
+ type IAtmosphereParameterPatch = Partial<IAtmosphereParameters>;
175
+ interface ISolarPositionInput {
176
+ readonly date?: Date | string;
177
+ readonly dayOfYear?: number;
178
+ readonly timeOfDay?: number;
179
+ readonly latitude: number;
180
+ readonly longitude: number;
181
+ /** UTC offset in hours. Use zero when `date` is already UTC. */
182
+ readonly utcOffset?: number;
183
+ }
184
+ interface ISolarPosition {
185
+ elevation: number;
186
+ azimuth: number;
187
+ }
188
+ /** Validate and clone game-owned coefficients. There is intentionally no Earth fallback.
189
+ * @situation validate atmosphere coefficients before a game creates its sky
190
+ * @constraint provide all three coefficient vectors and both radii; omitted fields are errors
191
+ * @example const parameters = resolveAtmosphereParameters({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
192
+ */
193
+ declare function resolveAtmosphereParameters(options: IAtmosphereParameters): IResolvedAtmosphereParameters;
194
+ /** Apply a partial game-owned atmosphere change while preserving validation.
195
+ * @situation change scattering coefficients and rebake an atmosphere
196
+ * @constraint patches cannot introduce omitted, negative, or non-finite physical values
197
+ * @example atmosphere.setAtmosphere({ rayleigh: [0.008, 0.016, 0.04] });
198
+ */
199
+ declare function updateAtmosphereParameters(current: IResolvedAtmosphereParameters, patch: IAtmosphereParameterPatch): IResolvedAtmosphereParameters;
200
+ /**
201
+ * Return the direct vertical transmittance of the supplied atmosphere.
202
+ *
203
+ * The coefficient fixture follows the Hillaire/Bruneton Earth model: an exponential Rayleigh
204
+ * column, an exponential aerosol column, and the triangular ozone column. This CPU value is a
205
+ * small validation oracle for the same coefficients that the GPU LUT kernels consume.
206
+ * @situation check a supplied atmosphere's direct vertical transmittance
207
+ * @constraint use the returned value as a validation oracle; the rendered path samples the LUT
208
+ * @example const zenith = zenithTransmittance({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
209
+ */
210
+ declare function zenithTransmittance(parameters: IAtmosphereParameters | IResolvedAtmosphereParameters): AtmosphereRgb;
211
+ /** Approximate direct transmittance for a ray leaving the ground in a supplied direction.
212
+ * @situation colour a game-owned sun from atmosphere extinction
213
+ * @constraint pass a non-zero direction; coefficients and radii come from the game
214
+ * @example const transmittance = directionalTransmittance(parameters, sunDirection);
215
+ */
216
+ declare function directionalTransmittance(parameters: IAtmosphereParameters | IResolvedAtmosphereParameters, direction: Vector3): Vector3;
217
+ /** Calculate solar elevation and azimuth from time, latitude, and longitude.
218
+ * @situation move a sun across a real day at a game's latitude and longitude
219
+ * @constraint dates are interpreted as UTC unless utcOffset is supplied; no fixed sun direction is assumed
220
+ * @constraint pass a mutable { azimuth, elevation } target to reuse the result object in a steady frame loop
221
+ * @example const sun = solarPosition({ date, latitude: 49.28, longitude: -123.12, utcOffset: -8 });
222
+ */
223
+ declare function solarPosition(input: ISolarPositionInput, target?: ISolarPosition): ISolarPosition;
224
+ declare function solarPosition(date: Date | string, latitude: number, longitude: number, target?: ISolarPosition): ISolarPosition;
225
+ /** Convert solar elevation and azimuth degrees into a normalized Three.js direction.
226
+ * @situation aim a template's sun from solarPosition output
227
+ * @constraint elevation and azimuth must be finite degrees
228
+ * @example const direction = directionFromSolarPosition(sun.elevation, sun.azimuth);
229
+ */
230
+ declare function directionFromSolarPosition(elevation: number, azimuth: number): Vector3;
231
+
232
+ interface IAtmosphereLutResolution {
233
+ readonly width: number;
234
+ readonly height: number;
235
+ }
236
+ interface IAtmosphereLutResolutions {
237
+ readonly transmittance: IAtmosphereLutResolution;
238
+ readonly multiScattering: IAtmosphereLutResolution;
239
+ readonly skyView: IAtmosphereLutResolution;
240
+ }
241
+ /** The reference dimensions; games may provide smaller dimensions when their startup budget says so. */
242
+ declare const ATMOSPHERE_LUT_RESOLUTIONS: IAtmosphereLutResolutions;
243
+ declare const LUT_RESOLUTIONS: IAtmosphereLutResolutions;
244
+ interface IParameterUniforms {
245
+ readonly atmosphereRadius: UniformNode<"float", number>;
246
+ readonly mie: UniformNode<"vec3", three.Vector3>;
247
+ readonly ozone: UniformNode<"vec3", three.Vector3>;
248
+ readonly planetRadius: UniformNode<"float", number>;
249
+ readonly rayleigh: UniformNode<"vec3", three.Vector3>;
250
+ }
251
+ /** Resolve the three LUT dimensions, allowing a game to trade startup cost for resolution.
252
+ * @situation choose atmosphere LUT dimensions for a measured startup budget
253
+ * @constraint every width and height must be a positive integer; the dimensions are not a named fidelity tier
254
+ * @example const resolutions = resolveAtmosphereLutResolutions({ skyView: { width: 128, height: 72 } });
255
+ */
256
+ declare function resolveAtmosphereLutResolutions(resolutions: Partial<IAtmosphereLutResolutions> | undefined): IAtmosphereLutResolutions;
257
+ /** Own the transmittance, multi-scattering, and sky-view compute lookup textures.
258
+ * @situation bake the three atmosphere LUTs once before a game shows its world
259
+ * @constraint supply all physical parameters; this class creates no scene appearance
260
+ * @example const luts = new AtmosphereLuts({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
261
+ */
262
+ declare class AtmosphereLuts {
263
+ #private;
264
+ readonly resolutions: IAtmosphereLutResolutions;
265
+ readonly transmittance: StorageTexture;
266
+ readonly multiScattering: StorageTexture;
267
+ readonly skyView: StorageTexture;
268
+ readonly warmupNodes: readonly ComputeNode[];
269
+ readonly uniforms: IParameterUniforms;
270
+ constructor(parameters: IAtmosphereParametersLike, resolutions?: Partial<IAtmosphereLutResolutions>);
271
+ get hash(): string;
272
+ update(parameters: IAtmosphereParametersLike): void;
273
+ sampleTransmittance(uv: Node<"vec2">): TextureNode;
274
+ sampleSkyView(uv: Node<"vec2">): TextureNode;
275
+ dispose(): void;
276
+ }
277
+ type IAtmosphereParametersLike = IResolvedAtmosphereParameters | IAtmosphereParameters;
278
+
279
+ /** The structural contract consumed by the compute registry from PRD-242. */
280
+ interface IComputeDriven$1 {
281
+ readonly warmupNodes: readonly unknown[];
282
+ attachRenderer(renderer: IRendererLike): void;
283
+ readonly processCadence?: "fixed" | "render";
284
+ process(renderer: IRendererLike): void;
285
+ detach(): void;
286
+ readonly released: boolean;
287
+ }
288
+ interface IAtmosphereOptions extends IAtmosphereParameters {
289
+ readonly resolutions?: Partial<IAtmosphereLutResolutions>;
290
+ }
291
+ interface IAtmosphereScenePass {
292
+ getTextureNode(name?: string): Node<"vec4">;
293
+ }
294
+ type AtmosphereDirection = Vector3 | readonly [number, number, number] | Node<"vec3">;
295
+ /**
296
+ * Own the compute lifetime and expose only parameter-driven atmosphere nodes.
297
+ *
298
+ * The class deliberately creates no mesh, material, or scene light. A template chooses all of
299
+ * those, and the same object remains useful when a game supplies a completely different look.
300
+ * @situation render a sunrise that changes as time and place change
301
+ * @situation add distance haze from the depth of a scene pass
302
+ * @constraint supply rayleigh, mie, ozone, planetRadius, and atmosphereRadius; there is no Earth fallback
303
+ * @constraint the game creates the sky object, surface, and sun from the returned nodes
304
+ * @example const atmosphere = new Atmosphere({ rayleigh, mie, ozone, planetRadius, atmosphereRadius });
305
+ * ctx.add(atmosphere);
306
+ */
307
+ declare class Atmosphere extends Group implements IComputeDriven$1 {
308
+ #private;
309
+ readonly luts: AtmosphereLuts;
310
+ constructor(options: IAtmosphereOptions);
311
+ get parameters(): IResolvedAtmosphereParameters;
312
+ get warmupNodes(): readonly ComputeNode[];
313
+ get released(): boolean;
314
+ get hash(): string;
315
+ attachRenderer(renderer: IRendererLike): void;
316
+ process(renderer?: IRendererLike | undefined): void;
317
+ detach(): void;
318
+ setAtmosphere(patch: IAtmosphereParameterPatch): this;
319
+ setCoefficients(patch: IAtmosphereParameterPatch): this;
320
+ setSunDirection(elevation: number, azimuth: number): this;
321
+ setSunDirection(direction: Vector3): this;
322
+ setSunDirection(position: Pick<ISolarPosition, "elevation" | "azimuth">): this;
323
+ getSunDirection(target?: Vector3): Vector3;
324
+ /** Return the game-owned sky radiance lookup as a TSL vec3 or a CPU validation sample. */
325
+ radiance(direction: AtmosphereDirection): Node<"vec3"> | Vector3;
326
+ /** Return direct solar transmittance as a TSL vec3 or a CPU validation sample. */
327
+ sunTransmittance(direction: AtmosphereDirection): Node<"vec3"> | Vector3;
328
+ /**
329
+ * Composite scene colour against game-supplied in-scattered radiance using scene-pass depth.
330
+ *
331
+ * The optional radiance node lets the game apply its own exposure or artistic tint. Leaving it
332
+ * out uses the raw unit-illumination LUT value and does not introduce a framework look.
333
+ */
334
+ aerialPerspective(scenePass: IAtmosphereScenePass, depth: Node<"float"> | number, inScatteredRadiance?: Node<"vec3"> | Vector3): Node<"vec4">;
335
+ }
336
+
337
+ /** Calculate solar elevation and azimuth for one UTC date.
338
+ * @situation calculate a sun direction from a timestamp and a game location
339
+ * @constraint dates are interpreted as UTC; use solarPosition for an explicit local offset
340
+ * @example const sun = solarPositionAt(new Date(), 49.28, -123.12);
341
+ */
342
+ declare function solarPositionAt(date: Date | string, latitude: number, longitude: number): ISolarPosition;
343
+
344
+ /**
345
+ * The lifecycle contract for a game-owned GPU simulation.
346
+ *
347
+ * A compute-driven object owns its kernels, buffers, and appearance. The framework only attaches
348
+ * the active renderer, warms the kernels before the world is shown, dispatches process calls at
349
+ * the object's declared cadence, and releases the object when its scene ends.
350
+ */
351
+ interface IComputeDriven {
352
+ /** Kernels to compile before the world is shown. Read once, at attach. */
353
+ readonly warmupNodes: readonly unknown[];
354
+ attachRenderer(renderer: IRendererLike): void;
355
+ /**
356
+ * The loop phase that dispatches `process`. Defaults to fixed-step; render cadence preserves the
357
+ * existing behavior of consumers whose simulation is intentionally tied to presentation.
358
+ */
359
+ readonly processCadence?: "fixed" | "render";
360
+ /** Dispatched once per fixed step, in scene-add order unless render cadence is declared. */
361
+ process(renderer: IRendererLike): void;
362
+ detach(): void;
363
+ readonly released: boolean;
364
+ }
365
+ /** The ordered registry used by the game loop for all compute-driven scene objects. */
366
+ declare class ComputeDrivenRegistry {
367
+ #private;
368
+ get size(): number;
369
+ /** Attach and remember one object. Re-adding the same object is idempotent. */
370
+ add(object: Object3D & IComputeDriven, renderer: IRendererLike): void;
371
+ /** Release one object without disturbing the order of the remaining objects. */
372
+ remove(driven: IComputeDriven): void;
373
+ /** Kernels in the same order as their objects were added to the scene. */
374
+ get warmupNodes(): readonly unknown[];
375
+ /** Dispatch fixed-step objects once; detached scene children are released before dispatch. */
376
+ process(renderer: IRendererLike): void;
377
+ /** Dispatch render-cadence objects once; detached scene children are released before dispatch. */
378
+ processRender(renderer: IRendererLike): void;
379
+ /** Release every registered object, continuing after a failure so no resource is stranded. */
380
+ clear(): void;
381
+ }
382
+
383
+ interface IGPUReadbackOptions {
384
+ /** The GPU storage attribute to copy — a TSL storage node's `.value`. */
385
+ readonly attribute: unknown;
386
+ /**
387
+ * Frames between readback requests. `1` asks every frame; larger values throttle.
388
+ *
389
+ * A copy off the GPU costs a queue submission and a mapped buffer, so a game reading a field it
390
+ * only consults for physics asks for it every few frames and pays the staleness instead.
391
+ */
392
+ readonly everyFrames: number;
393
+ }
394
+ /** A landed copy of GPU memory, with the age of the frame that produced it. */
395
+ interface IGPUReadbackSample {
396
+ readonly data: Float32Array;
397
+ /**
398
+ * Frames between the frame whose GPU state these bytes hold and the frame reading them.
399
+ *
400
+ * This is the number a caller must not be allowed to ignore. A buoyancy solver that treats a
401
+ * 4-frame-old surface as this frame's surface floats a hull through the water it is drawn on,
402
+ * and nothing in the frame says so.
403
+ */
404
+ readonly staleFrames: number;
405
+ }
406
+ /**
407
+ * A throttled, fire-and-forget copy of a GPU buffer into CPU memory.
408
+ *
409
+ * Every sample it hands back carries its own age. That is the whole point: the copy is
410
+ * asynchronous, so the bytes are always some frames behind the GPU, and a class that hid that
411
+ * would let a caller mistake stale data for live data with no way to find out.
412
+ *
413
+ * `request()` never awaits and never blocks the frame. One copy is in flight at a time; requests
414
+ * made while one is pending are dropped rather than queued, because a backlog of copies of a field
415
+ * that has already moved on is latency with no information in it.
416
+ */
417
+ declare class GPUReadback {
418
+ #private;
419
+ readonly everyFrames: number;
420
+ constructor(options: IGPUReadbackOptions);
421
+ get released(): boolean;
422
+ /** True while a copy is in flight. A game that wants to pace its own work can read it. */
423
+ get pending(): boolean;
424
+ /** The newest landed bytes, or `undefined` before the first copy lands. */
425
+ get data(): Float32Array | undefined;
426
+ /**
427
+ * How many frames old the landed bytes are.
428
+ *
429
+ * Before anything has landed this is the number of frames since construction, which grows
430
+ * without bound on purpose: "no data yet" and "data from frame zero" must not read the same.
431
+ */
432
+ get staleFrames(): number;
433
+ /** The newest bytes with their age attached, or `undefined` before the first copy lands. */
434
+ get sample(): IGPUReadbackSample | undefined;
435
+ /** Requests, landings and failures, for a report that has to say why a sample is old. */
436
+ get stats(): {
437
+ readonly requests: number;
438
+ readonly lands: number;
439
+ readonly failures: number;
440
+ };
441
+ /**
442
+ * Advances the frame clock and starts a copy when the throttle allows one.
443
+ *
444
+ * Safe to call every frame. It returns before the GPU has answered — awaiting it is the one
445
+ * thing that would turn this class into the stall it exists to avoid.
446
+ */
447
+ request(renderer: IRendererLike): void;
448
+ /** Drops the attribute reference and the landed bytes. Further requests throw. */
449
+ dispose(): void;
450
+ }
451
+
452
+ interface IClothTopologyOptions {
453
+ /** Original geometry vertex indices that never move. Duplicate positions pin together. */
454
+ readonly pinned: readonly number[];
455
+ }
456
+
457
+ /** Packed collision input supplied by an optional dependency such as `@threenative/physics`. */
458
+ interface ISoftBodyCollision {
459
+ readonly capacity: number;
460
+ writeBoxes(target: Float32Array, worldToLocal: Matrix4): number;
461
+ }
462
+ interface ISoftBody3DOptions extends IClothTopologyOptions {
463
+ /** Spring acceleration per metre of stretch. Required; the game owns the cloth response. */
464
+ readonly stiffness: number;
465
+ /** Exponential velocity decay per second. Required; zero disables damping. */
466
+ readonly damping: number;
467
+ /** Local-space acceleration in metres per second squared. */
468
+ readonly gravity: readonly [number, number, number];
469
+ /** Local-space wind acceleration in metres per second squared. */
470
+ readonly wind: readonly [number, number, number];
471
+ /** Existing physics bodies translated by the physics package; omitted when cloth has no world collision. */
472
+ readonly collision?: ISoftBodyCollision;
473
+ /** Framework fixed-step duration. Defaults to the engine convention of 1/60 second. */
474
+ readonly timeStep?: number;
475
+ /** Throttled GPU position readback for gameplay/proof; zero disables it. */
476
+ readonly readbackEveryFrames?: number;
477
+ }
478
+ /**
479
+ * Simulate an ordinary game-authored triangle mesh as cloth on the existing fixed-step GPU lane.
480
+ *
481
+ * The mesh supplies every visible choice. This class welds exporter duplicates, owns spring and
482
+ * position buffers, and replaces only the cloned material's position node. It adds no material,
483
+ * colour, texture, wind, stiffness, damping, or pinning default.
484
+ *
485
+ * @situation make a flag, cape, or curtain move as cloth
486
+ * @situation simulate a deforming surface while keeping one edge pinned
487
+ * @situation simulate cloth sails blowing in the wind
488
+ * @situation make cloth sails billow in wind on a ship
489
+ * @constraint the mesh must use one Three.js node material and contain complete triangles
490
+ * @constraint pinned, stiffness, damping, gravity, and wind are required game-owned inputs
491
+ * @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
492
+ * @override timeStep follows the engine 1/60-second convention unless the game overrides it
493
+ * @override readbackEveryFrames enables an explicitly stale CPU position sample; zero disables it
494
+ * @example const flag = new SoftBody3D(flagMesh, { pinned: topEdge, stiffness: 35, damping: 1.8, gravity: [0, -9.81, 0], wind: [1.5, 0, 0.4] });
495
+ */
496
+ declare class SoftBody3D extends Mesh<BufferGeometry, NodeMaterial> implements IComputeDriven {
497
+ #private;
498
+ readonly processCadence: "fixed";
499
+ readonly warmupNodes: readonly ComputeNode[];
500
+ readonly stiffness: number;
501
+ readonly damping: number;
502
+ readonly timeStep: number;
503
+ readonly gravity: Vector3;
504
+ readonly wind: Vector3;
505
+ readonly uniqueVertexCount: number;
506
+ readonly springCount: number;
507
+ constructor(mesh: Mesh, options: ISoftBody3DOptions);
508
+ get released(): boolean;
509
+ get steps(): number;
510
+ /** Latest asynchronous GPU positions and their age, when readback was requested. */
511
+ get sample(): IGPUReadbackSample | undefined;
512
+ debug(): Record<string, unknown>;
513
+ attachRenderer(renderer: IRendererLike): void;
514
+ process(renderer?: IRendererLike | undefined): void;
515
+ detach(): void;
516
+ }
517
+
518
+ /** Selects the meshes that become part of a GPU trace set. */
519
+ interface IGPUSceneBVHOptions {
520
+ readonly include?: (object: Mesh) => boolean;
521
+ }
522
+ /**
523
+ * A material range expressed in packed index elements, after the BVH leaf reorder.
524
+ *
525
+ * `start` and `count` address the uploaded index buffer, not the source geometry, so a single
526
+ * source material can appear as more than one range once the SAH sort interleaves its triangles.
527
+ */
528
+ interface IGPUSceneBVHMaterialGroup {
529
+ readonly count: number;
530
+ readonly materialIndex: number;
531
+ readonly start: number;
532
+ }
533
+ /** The upstream TSL ray query, exposed without renaming or wrapping it. */
534
+ type GPUSceneBVHTraceFunction = (...args: readonly unknown[]) => unknown;
535
+ declare const bvhIntersectFirstHit: GPUSceneBVHTraceFunction;
536
+ declare const rayStruct: StructTypeNode;
537
+ /**
538
+ * Snapshot selected scene meshes into world-space storage buffers for an upstream TSL BVH query.
539
+ *
540
+ * This class owns packing, residency, and release. It deliberately does not own the ray query or
541
+ * a rendered effect: a game imports the exact upstream `bvhIntersectFirstHit` and `rayStruct`
542
+ * exports through the core entry point and uses these four named nodes in its own `src/render/`
543
+ * kernel. The snapshot is static until `rebuild()`.
544
+ */
545
+ declare class GPUSceneBVH extends Group implements IComputeDriven {
546
+ #private;
547
+ readonly indices: StorageBufferNode<"uvec3">;
548
+ readonly nodes: StorageBufferNode<"struct">;
549
+ readonly normals: StorageBufferNode<"vec3">;
550
+ readonly positions: StorageBufferNode<"vec3">;
551
+ readonly warmupNodes: readonly unknown[];
552
+ constructor(scene: Object3D, options?: IGPUSceneBVHOptions);
553
+ get buildMs(): number;
554
+ get materialGroups(): readonly IGPUSceneBVHMaterialGroup[];
555
+ get objectCount(): number;
556
+ get released(): boolean;
557
+ get triangleCount(): number;
558
+ get vertexCount(): number;
559
+ attachRenderer(renderer: IRendererLike): void;
560
+ process(_renderer: IRendererLike): void;
561
+ /** Repack the selected scene objects and replace the GPU buffers behind the stable node handles. */
562
+ rebuild(): void;
563
+ /** Dispose every storage attribute owned by this snapshot. Safe to call more than once. */
564
+ detach(): void;
565
+ }
566
+
567
+ /** One instance's transform, in the units the geometry was authored in. */
568
+ interface IInstancedPlacement {
569
+ /** World position of the instance's origin, as `[x, y, z]`. */
570
+ readonly position: readonly [number, number, number];
571
+ /** Euler rotation in radians, as `[x, y, z]`. Default none. */
572
+ readonly rotation?: readonly [number, number, number];
573
+ /** Per-axis scale as `[x, y, z]`, or one number for all three. Default 1. */
574
+ readonly scale?: readonly [number, number, number] | number;
575
+ }
576
+ interface IInstancedBatchOptions {
577
+ /** The shape every instance draws, supplied by the game. */
578
+ readonly geometry: BufferGeometry;
579
+ /**
580
+ * The surface every instance draws with, supplied by the game and used by reference — recolour
581
+ * it and the whole batch recolours. Required: collapsing the draws is the engine's job, what
582
+ * the props look like never is.
583
+ */
584
+ readonly material: Material;
585
+ }
586
+ interface IInstancedBatchBuildOptions {
587
+ /** Passed straight to the built mesh. Default `false`, as in Three.js. */
588
+ readonly castShadow?: boolean;
589
+ /** Name on the built mesh, so a capture or a scene dump can tell one batch from another. */
590
+ readonly name?: string;
591
+ /** Added to this object when the batch builds. Omit to take the mesh and place it yourself. */
592
+ readonly parent?: Object3D;
593
+ /** Passed straight to the built mesh. Default `false`, as in Three.js. */
594
+ readonly receiveShadow?: boolean;
595
+ }
596
+ /**
597
+ * Collapses many copies of one shape into a single draw, without knowing the count up front.
598
+ *
599
+ * `new InstancedMesh(geometry, material, count)` needs `count` before anything has been placed, so
600
+ * a procedural builder either walks its own layout twice, over-allocates and fixes `.count`
601
+ * afterwards, or gathers transforms into an array first. This is that array, with the
602
+ * `Object3D`-scratch-and-`updateMatrix` dance and the post-fill bookkeeping — `instanceMatrix`
603
+ * invalidation and a bounding sphere the culler can use — done once instead of at every site.
604
+ *
605
+ * It decides nothing about how the result looks: the shape, the surface and every transform are
606
+ * the game's, and the built mesh is handed back so the game can keep animating instances by index.
607
+ */
608
+ declare class InstancedBatch {
609
+ #private;
610
+ readonly geometry: BufferGeometry;
611
+ readonly material: Material;
612
+ constructor(options: IInstancedBatchOptions);
613
+ /** How many instances have been placed so far. */
614
+ get count(): number;
615
+ /** The built mesh, or `undefined` before {@link build} — never a guess. */
616
+ get mesh(): InstancedMesh | undefined;
617
+ /**
618
+ * Records one instance from a matrix the game composed itself, and returns its instance index.
619
+ *
620
+ * The matrix is copied, so the caller may reuse a single scratch `Matrix4` across every call.
621
+ */
622
+ add(matrix: Matrix4): number;
623
+ /** Records one instance from position, scale and Euler rotation, and returns its instance index. */
624
+ place(placement: IInstancedPlacement): number;
625
+ /**
626
+ * Records one instance stretched between two points, and returns its instance index.
627
+ *
628
+ * Chains, tie rods, railing bars, struts and cables are all "from A to B" rather than "at P with
629
+ * rotation R". Deriving the orientation here is what keeps every caller from hand-computing an
630
+ * Euler angle that goes wrong the moment one endpoint moves.
631
+ */
632
+ span(from: readonly [number, number, number], to: readonly [number, number, number], radius: number): number;
633
+ /**
634
+ * Turns everything placed so far into one `InstancedMesh`, and returns it.
635
+ *
636
+ * Returns `undefined` when nothing was placed. That is deliberate: `new InstancedMesh(g, m, 0)`
637
+ * satisfies every type check and draws nothing, so a builder whose layout produced no instances
638
+ * would look identical to one that worked. `undefined` puts that case in the caller's types.
639
+ */
640
+ build(options?: IInstancedBatchBuildOptions): InstancedMesh | undefined;
641
+ }
642
+
643
+ /**
644
+ * A mesh that draws only the clusters this camera can resolve.
645
+ *
646
+ * The asset pipeline bakes a cluster DAG into the `.glb` (`TN_virtual_geometry`); the loader returns
647
+ * one of these when it finds one, and an ordinary `Mesh` when it does not. Nothing about how the
648
+ * mesh looks lives here: `geometry`, `material` and every appearance parameter are the game's, and
649
+ * swapping the surface at any time swaps what draws.
650
+ *
651
+ * The rule, and it asks a cluster nothing about its neighbours: **draw a cluster when its own error
652
+ * projects to fewer screen pixels than the threshold, and its parent group's does not.** Each side
653
+ * is projected through the sphere of the group it belongs to, which is what keeps the cut watertight
654
+ * as the camera moves — a group's sphere encloses every child's, so the parent's projected error can
655
+ * never fall below a child's.
656
+ */
657
+ /** The baked payload, exactly as `TN_virtual_geometry` stores it. */
658
+ interface IClusterTable {
659
+ /** Per cluster, `[centreX, centreY, centreZ, radius]`. Culling reads it; PRD-283 will. */
660
+ readonly bounds: Float32Array;
661
+ /** Per cluster, `[axisX, axisY, axisZ, cutoff]`. */
662
+ readonly cones: Float32Array;
663
+ /** Per cluster, `[ownError, parentError]`, in the mesh's own units. */
664
+ readonly errors: Float32Array;
665
+ /** Cluster-ordered triangles for every level, indexing the geometry's vertex buffer. */
666
+ readonly indices: Uint32Array;
667
+ /** Per cluster, the sphere `parentError` is projected through. */
668
+ readonly parentSpheres: Float32Array;
669
+ /** Per cluster, `[start, count]` into {@link IClusterTable.indices}. */
670
+ readonly ranges: Uint32Array;
671
+ /** Per cluster, the sphere its own error is projected through. */
672
+ readonly sourceSpheres: Float32Array;
673
+ }
674
+ interface IClusteredMeshOptions {
675
+ /**
676
+ * Screen-space error a cluster may show, in pixels, before its children are drawn instead.
677
+ *
678
+ * One pixel is the honest default: the point of the technique is that what the camera cannot
679
+ * resolve is never submitted. Raising it trades fidelity for triangles, and the number is the
680
+ * game's to choose.
681
+ */
682
+ readonly errorPixels?: number;
683
+ /**
684
+ * How far the camera must move, in the mesh's own units, before the cut is taken again.
685
+ *
686
+ * Popping is a defect, not a tuning parameter: a camera standing still and breathing must not
687
+ * flip a cluster back and forth. Below this the previous cut is kept, which is always a cut some
688
+ * camera would have chosen and therefore always watertight. Default is a thousandth of the mesh's
689
+ * radius.
690
+ */
691
+ readonly recutDistance?: number;
692
+ }
693
+ declare class ClusteredMesh extends Mesh {
694
+ #private;
695
+ /** The baked payload. Read-only at run time; the bake is the only thing that writes it. */
696
+ readonly table: IClusterTable;
697
+ /** Screen-space error budget in pixels. Writable — it is the game's call. */
698
+ errorPixels: number;
699
+ /** Camera movement, in the mesh's own units, below which the previous cut is kept. */
700
+ recutDistance: number;
701
+ constructor(geometry: BufferGeometry, surface: Material | Material[], table: IClusterTable, options?: IClusteredMeshOptions);
702
+ /** Clusters in the current cut. */
703
+ get drawnClusters(): number;
704
+ /** Triangles the current cut submits. */
705
+ get drawnTriangles(): number;
706
+ /**
707
+ * Chooses this frame's cut and compacts it into one index range.
708
+ *
709
+ * Called by the game before it renders, not from `onBeforeRender`: an empty cut has to skip the
710
+ * draw rather than submit a zero-count one, and by the time three calls `onBeforeRender` the draw
711
+ * is already on the list. A mesh this leaves invisible is made visible again by the next call,
712
+ * which is why the call belongs in the frame loop rather than in the renderer.
713
+ *
714
+ * @returns triangles the mesh will draw.
715
+ */
716
+ update(camera: Camera, viewportHeight: number): number;
717
+ }
718
+ /**
719
+ * Takes every clustered mesh and every clustered batch under `root` through this frame's cut.
720
+ *
721
+ * The engine calls this itself, once a frame, before the render — virtual geometry ships on and a
722
+ * game that has to remember to call something has not been given it. A scene holding neither costs
723
+ * one traversal that finds nothing.
724
+ *
725
+ * @returns triangles the clustered meshes and batches will submit.
726
+ */
727
+ declare function updateClusteredMeshes(root: {
728
+ traverse(callback: (object: object) => void): void;
729
+ }, camera: Camera, viewportHeight: number): number;
730
+
731
+ /**
732
+ * Many copies of one clustered body, each drawn at the detail its own distance earns.
733
+ *
734
+ * `InstancedBatch` collapses repeated props into one draw; this does the same for a body that
735
+ * carries a cluster DAG, and adds the part `InstancedBatch` cannot do — a copy twelve metres away
736
+ * and a copy two hundred metres away do not draw the same triangles.
737
+ *
738
+ * **Why instances are bucketed rather than cut one by one.** One indexed draw has one index range,
739
+ * and multi-draw indirect is not portably available on this stack, so *n* different cuts would mean
740
+ * *n* draws and *n* index buffers — on four hundred boulders that is a gigabyte of index data to
741
+ * save vertex work. Instead the copies are grouped by distance, one cut is taken per occupied
742
+ * group, and each group draws as one instanced draw. Every group's cut is a real cut of the DAG and
743
+ * therefore watertight; the group is cut at the distance of its *nearest* member, so no copy is ever
744
+ * drawn coarser than its own distance allows — only finer, by at most the width of one group.
745
+ *
746
+ * Geometry, surface and every transform are the game's, exactly as with `InstancedBatch`.
747
+ */
748
+ /** One copy's transform, in the units the geometry was authored in. */
749
+ interface IClusteredPlacement {
750
+ readonly position: readonly [number, number, number];
751
+ readonly rotation?: readonly [number, number, number];
752
+ readonly scale?: readonly [number, number, number] | number;
753
+ }
754
+ interface IClusteredBatchOptions {
755
+ /**
756
+ * How far the camera must move, as a fraction of its distance to the nearest copy, before the
757
+ * cut is taken again. Default 1%.
758
+ *
759
+ * The engine cuts every batch every frame, so this is the difference between a walk that costs a
760
+ * few hundred cluster tests and one that costs half a million. A camera that has not moved
761
+ * meaningfully keeps the previous cut, which is always a cut some camera would have chosen and
762
+ * therefore always watertight.
763
+ */
764
+ readonly recutFraction?: number;
765
+ /**
766
+ * Ratio between one distance group and the next, above 1.
767
+ *
768
+ * Narrower groups follow each copy's own distance more closely and cost one more draw each.
769
+ * 1.25 is the default: about a dozen groups across a scene that spans a few hundred metres.
770
+ */
771
+ readonly distanceRatio?: number;
772
+ /** Screen-space error budget in pixels, as {@link ClusteredMesh}. Default 1. */
773
+ readonly errorPixels?: number;
774
+ /** The shape every copy draws, carrying the baked cluster table. */
775
+ readonly geometry: BufferGeometry;
776
+ /** The surface every copy draws with, supplied by the game and used by reference. */
777
+ readonly material: Material;
778
+ /** The bake, exactly as `TN_virtual_geometry` stores it. */
779
+ readonly table: IClusterTable;
780
+ }
781
+ interface IClusteredBatchBuildOptions {
782
+ readonly castShadow?: boolean;
783
+ readonly name?: string;
784
+ readonly parent: Object3D;
785
+ readonly receiveShadow?: boolean;
786
+ }
787
+ declare class ClusteredBatch {
788
+ #private;
789
+ constructor(options: IClusteredBatchOptions);
790
+ /** How many copies are placed. */
791
+ get count(): number;
792
+ /** Draws this batch will submit — one per occupied distance group. */
793
+ get drawCalls(): number;
794
+ /** Triangles the current cut submits, over every copy. */
795
+ get drawnTriangles(): number;
796
+ /** Adds one copy. Returns its index, so the game can move it later. */
797
+ place(placement: IClusteredPlacement): number;
798
+ /** Attaches the batch to the scene. Nothing draws until {@link ClusteredBatch.update} runs. */
799
+ build(options: IClusteredBatchBuildOptions): Object3D;
800
+ /**
801
+ * Chooses this frame's cut for every distance group.
802
+ *
803
+ * @returns triangles the batch will submit.
804
+ */
805
+ update(camera: Camera, viewportHeight: number): number;
806
+ }
807
+
808
+ /** The atlas has one copied edge texel on either side of each packed SH sub-volume. */
809
+ declare const ATLAS_PADDING = 1;
810
+ /** The machine-readable marker emitted whenever the probe state changes. */
811
+ declare const PROBE_VOLUME_MARKER = "TN_PROBE_VOLUME";
812
+ type IVector3Like = {
813
+ readonly x: number;
814
+ readonly y: number;
815
+ readonly z: number;
816
+ };
817
+ type IProbePosition = Vector3 | Node<"vec3">;
818
+ /** One RGB L2 spherical-harmonic coefficient, in the upstream probe ordering. */
819
+ interface IProbeVolumeCoefficient {
820
+ readonly r: number;
821
+ readonly g: number;
822
+ readonly b: number;
823
+ }
824
+ /** Probe density in probes per world unit, either isotropic or per-axis. */
825
+ type ProbeVolumeDensity = number | readonly [number, number, number] | IVector3Like;
826
+ interface IProbeVolumeOptions {
827
+ /** World-space bounds; the volume does not move with the object after construction. */
828
+ readonly bounds: Box3 | {
829
+ readonly min: IVector3Like;
830
+ readonly max: IVector3Like;
831
+ };
832
+ /** Probe spacing expressed as probes per world unit. */
833
+ readonly density: ProbeVolumeDensity;
834
+ /** Optional device limit supplied by a host that knows it before construction. */
835
+ readonly maxTextureDimension3D?: number;
836
+ /** Alias for integrations that expose the WebGPU limit under a device-oriented name. */
837
+ readonly deviceTextureLimit?: number;
838
+ /** Maximum wall-clock work per render phase, in milliseconds. */
839
+ readonly bakeBudgetMs?: number;
840
+ /** Additional guard that keeps a clock-less host from processing an unbounded queue. */
841
+ readonly maxWorkItemsPerFrame?: number;
842
+ /** Resolution of each captured cube face. The static-lighting default is intentionally small. */
843
+ readonly cubemapSize?: number;
844
+ readonly near?: number;
845
+ readonly far?: number;
846
+ /** Additional indirect passes after the direct-light pass. */
847
+ readonly bounces?: number;
848
+ /** Injectable clock for deterministic scheduling tests. */
849
+ readonly now?: () => number;
850
+ readonly report?: (line: string) => void;
851
+ }
852
+ interface IProbeVolumeBakeProgress {
853
+ readonly completed: number;
854
+ readonly total: number;
855
+ readonly fraction: number;
856
+ readonly probesCompleted: number;
857
+ readonly probesTotal: number;
858
+ readonly pass: number;
859
+ readonly passes: number;
860
+ }
861
+ interface IProbeVolumeObservation {
862
+ readonly marker: typeof PROBE_VOLUME_MARKER;
863
+ readonly status: "unbaked" | "baking" | "ready";
864
+ readonly stale: boolean;
865
+ readonly unbaked: boolean;
866
+ /** `null` means no completed bake has established an age yet. */
867
+ readonly stalenessFrames: number | null;
868
+ readonly probeCount: number;
869
+ readonly atlasBytes: number;
870
+ readonly atlas: {
871
+ readonly width: number;
872
+ readonly height: number;
873
+ readonly depth: number;
874
+ };
875
+ readonly bakeProgress: IProbeVolumeBakeProgress;
876
+ /** Wall-clock time spent by the most recent incremental render-phase slice. */
877
+ readonly bakeCostMs: number;
878
+ readonly bakeBudgetMs: number;
879
+ /** True while pass zero samples a black texture instead of a previous bake. */
880
+ readonly samplingIsolated: boolean;
881
+ }
882
+ /** Read a complete marker-shaped observation without accepting malformed data. */
883
+ declare function readProbeVolumeObservation(value: unknown): IProbeVolumeObservation | undefined;
884
+ /**
885
+ * A WebGPU irradiance probe volume.
886
+ *
887
+ * The volume owns placement, GPU bake scheduling and a single padded atlas. It owns no light,
888
+ * material or colour: every coefficient comes from the scene rendered by its cube cameras. Add it
889
+ * through `ctx.add()` so `process()` runs in the render phase measured by `FrameBudget`.
890
+ *
891
+ * Bakes are static-lighting-first and explicit. Call `requestBake(scene)` after lights and static
892
+ * geometry are authored; a completed bake is reused until the game requests another one.
893
+ */
894
+ declare class ProbeVolume extends Object3D implements IComputeDriven {
895
+ #private;
896
+ readonly isProbeVolume = true;
897
+ readonly processCadence: "render";
898
+ readonly warmupNodes: readonly unknown[];
899
+ readonly boundingBox: Box3;
900
+ readonly resolution: Vector3;
901
+ constructor(options: IProbeVolumeOptions);
902
+ get texture(): Data3DTexture;
903
+ get atlasDepth(): number;
904
+ get atlasBytes(): number;
905
+ get probeCount(): number;
906
+ get released(): boolean;
907
+ get atlasData(): Float32Array;
908
+ get observation(): IProbeVolumeObservation;
909
+ /** Attach the active renderer; only WebGPU has the 3D render-target contract this class needs. */
910
+ attachRenderer(renderer: IRendererLike): void;
911
+ /** Start or coalesce an incremental static bake for a scene. */
912
+ requestBake(scene: Scene, options?: {
913
+ readonly bounces?: number;
914
+ }): Promise<void>;
915
+ /** Alias that reads naturally at call sites that want an awaitable bake request. */
916
+ bake(scene: Scene, options?: {
917
+ readonly bounces?: number;
918
+ }): Promise<void>;
919
+ bake(renderer: IRendererLike, scene: Scene, options?: {
920
+ readonly bounces?: number;
921
+ }): Promise<void>;
922
+ /**
923
+ * Return the L2 irradiance node for a world position and world normal.
924
+ *
925
+ * `sample()` with no arguments is the material-friendly form and reads `positionWorld` and
926
+ * `normalWorld`. Passing numeric vectors is reserved for diagnostics and deterministic tests;
927
+ * it evaluates the same SH coefficients held by the atlas packer.
928
+ */
929
+ sample(): Node<"vec3">;
930
+ sample(position: Vector3, normal: Vector3): Vector3;
931
+ sample(position: IProbePosition, normal?: Vector3 | Node<"vec3">): Vector3 | Node<"vec3">;
932
+ sampleIrradiance(position: Vector3, normal: Vector3): Vector3;
933
+ /** The GPU graph used by `sample`; exposed so generated materials can compose it explicitly. */
934
+ sampleNode(position?: Node<"vec3">, normal?: Node<"vec3">): Node<"vec3">;
935
+ /** Internal data seam used by unit/conformance fixtures to seed a known SH atlas without a GPU. */
936
+ setProbeCoefficients(ix: number, iy: number, iz: number, coefficients: readonly IProbeVolumeCoefficient[]): void;
937
+ /** One bounded render-phase slice. The game loop calls this through IComputeDriven. */
938
+ process(renderer: IRendererLike): void;
939
+ detach(): void;
940
+ get paddedSlices(): number;
941
+ get maximumDimension(): number;
942
+ get totalWork(): number;
943
+ probePosition(ix: number, iy: number, iz: number, target?: Vector3): Vector3;
944
+ }
945
+
946
+ /** One band of the spectrum, drawn on its own patch. */
947
+ interface ISpectralOceanCascade {
948
+ /** The world-space edge length, in metres, this cascade's grid tiles across. */
949
+ readonly patchSize: number;
950
+ }
951
+ interface ISpectralOceanOptions {
952
+ /** Grid resolution per cascade. A power of two; the transform has no other shape. */
953
+ readonly resolution: number;
954
+ /**
955
+ * The cascades, largest patch first.
956
+ *
957
+ * Each one carries only the wavelengths the next-smaller patch cannot resolve, so the bands do
958
+ * not overlap and a wave is never counted twice. One cascade is a toy: the join between bands is
959
+ * where a spectral ocean visibly fails, so there is nothing to look at until there are two.
960
+ */
961
+ readonly cascades: readonly ISpectralOceanCascade[];
962
+ readonly windSpeed: number;
963
+ /** Wind heading in radians. */
964
+ readonly windDirection: number;
965
+ readonly gravity: number;
966
+ /** Overall spectrum scale. A wave height decision, so the game owns the number. */
967
+ readonly amplitude: number;
968
+ /**
969
+ * How sharply waves align with the wind, as the exponent on the directional spread.
970
+ *
971
+ * A spectrum-tuning number with no defensible default, so there is none.
972
+ */
973
+ readonly directionality: number;
974
+ /** Horizontal displacement scale. Zero is a pure heightfield; higher values sharpen crests. */
975
+ readonly choppiness: number;
976
+ /** Waves shorter than this are cut off, in metres. */
977
+ readonly smallWaveCutoff: number;
978
+ readonly seed: number;
979
+ /**
980
+ * Which clock advances the simulation. Defaults to the game's fixed step.
981
+ *
982
+ * Fixed, because this sea is something the game reads: `sampleHeight` reports its age in frames,
983
+ * and a field advanced by the display makes that age mean a different amount of time on every
984
+ * machine. A game whose ocean is only ever looked at can pass `"render"` and pay for exactly the
985
+ * frames it draws.
986
+ */
987
+ readonly cadence?: "fixed" | "render";
988
+ /**
989
+ * The grid the CPU height query is sampled on, per side. Zero disables the query entirely.
990
+ *
991
+ * This is not the simulation resolution. It is the size of the only thing copied back off the
992
+ * GPU, so it is the whole cost of being able to float something: `readbackResolution` squared
993
+ * floats, every `readbackEveryFrames` frames.
994
+ */
995
+ readonly readbackResolution: number;
996
+ /** Frames between height copies. Ignored when `readbackResolution` is zero. */
997
+ readonly readbackEveryFrames: number;
998
+ }
999
+ /** A height read from the CPU copy, with the age of the frame that produced it. */
1000
+ interface ISpectralOceanHeight {
1001
+ readonly height: number;
1002
+ /**
1003
+ * Frames between the GPU state this height came from and now.
1004
+ *
1005
+ * A spectral ocean cannot offer an exact CPU height — there is no closed form, only texels the
1006
+ * GPU made — so this number is the contract. A caller that ignores it floats a hull on water
1007
+ * that is not the water being drawn, and nothing in the frame says so.
1008
+ */
1009
+ readonly staleFrames: number;
1010
+ }
1011
+ /**
1012
+ * A spectral ocean: cascaded wave spectra, inverse-transformed on the GPU every frame.
1013
+ *
1014
+ * It draws nothing. The game builds its own mesh and its own material and reads
1015
+ * `cascadeDisplacement(index)`; every colour, every foam threshold, every sky this water reflects
1016
+ * is the game's, and none of it can be changed from here.
1017
+ *
1018
+ * What it offers that an analytic wave function cannot is the look. What it cannot offer is an
1019
+ * exact CPU height: there is no closed form, only the texels the GPU produced, so `sampleHeight`
1020
+ * is a throttled copy that is always some frames behind and always says how many. A game that
1021
+ * needs the height to be exact wants an analytic field instead — that is a different contract, and
1022
+ * the reason this class has a different name rather than a flag.
1023
+ */
1024
+ declare class SpectralOcean extends Object3D implements IComputeDriven {
1025
+ #private;
1026
+ readonly resolution: number;
1027
+ readonly cascades: readonly ISpectralOceanCascade[];
1028
+ readonly processCadence: "fixed" | "render";
1029
+ readonly warmupNodes: readonly ComputeNode[];
1030
+ /** Floats copied off the GPU per readback, so a report can state the cost rather than imply it. */
1031
+ readonly readbackFloats: number;
1032
+ constructor(options: ISpectralOceanOptions);
1033
+ get released(): boolean;
1034
+ /** Simulation steps dispatched. A report that cannot count its own dispatches proves nothing. */
1035
+ get steps(): number;
1036
+ /** How old the CPU height copy is, or `undefined` when the height query is switched off. */
1037
+ get staleFrames(): number | undefined;
1038
+ /** The `(displaceX, height, displaceZ, fold)` buffer the game's material reads. */
1039
+ cascadeDisplacement(index: number): StorageBufferNode<"vec4">;
1040
+ /** The world-space tile size of one cascade, which the game's material needs to place it. */
1041
+ cascadePatchSize(index: number): number;
1042
+ /** Seconds of wave time. The game advances it, so a paused game has a paused sea. */
1043
+ advance(seconds: number): void;
1044
+ /**
1045
+ * The surface height at a world position, and how many frames behind it is.
1046
+ *
1047
+ * `undefined` until the first copy lands, and `undefined` forever when the height query was
1048
+ * switched off — never zero, because a hull floating at zero is indistinguishable from a hull
1049
+ * floating at sea level and that is exactly the mistake this must not allow.
1050
+ */
1051
+ sampleHeight(x: number, z: number): ISpectralOceanHeight | undefined;
1052
+ attachRenderer(renderer: IRendererLike): void;
1053
+ process(renderer?: IRendererLike | undefined): void;
1054
+ detach(): void;
47
1055
  }
48
1056
 
49
- interface GPUParticles3DBuffers {
1057
+ interface IGPUParticles3DBuffers {
50
1058
  readonly positions: StorageBufferNode<"vec3">;
51
1059
  readonly velocities: StorageBufferNode<"vec3">;
52
1060
  }
53
- interface GPUParticles3DOptions {
1061
+ interface IGPUParticles3DOptions {
54
1062
  readonly amount: number;
55
1063
  readonly material: SpriteNodeMaterial;
56
- readonly start: (buffers: GPUParticles3DBuffers) => ComputeNode;
57
- readonly process: (buffers: GPUParticles3DBuffers) => ComputeNode;
1064
+ readonly start: (buffers: IGPUParticles3DBuffers) => ComputeNode;
1065
+ readonly process: (buffers: IGPUParticles3DBuffers) => ComputeNode;
58
1066
  }
59
- declare class GPUParticles3D extends Sprite {
1067
+ declare class GPUParticles3D extends Sprite implements IComputeDriven {
60
1068
  #private;
61
1069
  readonly amount: number;
62
- readonly buffers: GPUParticles3DBuffers;
1070
+ readonly buffers: IGPUParticles3DBuffers;
1071
+ readonly processCadence: "render";
1072
+ readonly warmupNodes: readonly ComputeNode[];
63
1073
  emitting: boolean;
64
- constructor(options: GPUParticles3DOptions);
1074
+ constructor(options: IGPUParticles3DOptions);
65
1075
  get released(): boolean;
66
- attachRenderer(renderer: RendererLike): void;
67
- process(renderer?: RendererLike | undefined): void;
1076
+ attachRenderer(renderer: IRendererLike): void;
1077
+ process(renderer?: IRendererLike | undefined): void;
68
1078
  restart(): void;
69
1079
  detach(): void;
70
1080
  }
71
1081
 
1082
+ interface IFluidFieldVector2 {
1083
+ readonly x: number;
1084
+ readonly y: number;
1085
+ }
1086
+ interface IFluidFieldOptions {
1087
+ readonly resolution: number;
1088
+ readonly viscosity?: number;
1089
+ readonly pressureIterations?: number;
1090
+ readonly maxSplats?: number;
1091
+ readonly timeStep?: number;
1092
+ readonly vorticity?: number;
1093
+ readonly splatRadius?: number;
1094
+ }
1095
+ interface IFluidFieldSampler {
1096
+ sample(uv: Node$1<"vec2">): StorageTextureNode;
1097
+ }
1098
+ /**
1099
+ * Run a deterministic GPU fluid field whose data stays available to the game's render graph.
1100
+ *
1101
+ * The class is deliberately a scene object with the compute-driven lifecycle contract: adding it
1102
+ * to a game scene attaches the renderer, warm-up sees every pass, fixed steps dispatch the passes,
1103
+ * and removing it releases every GPU allocation. `dye` and `velocity` are read-only numeric
1104
+ * samplers; the game decides what those values become when drawn.
1105
+ * @situation simulate smoke, fire, fog, wind, or fluid response on a grid
1106
+ * @situation inject a touch, pointer, or gameplay impulse into a fluid field
1107
+ * @situation sample fluid dye or velocity in a game-owned render node
1108
+ * @constraint add the field through `ctx.add` so renderer attachment, fixed-step dispatch, and release are automatic
1109
+ * @constraint `dye` and `velocity` are numeric samplers; appearance stays in the game's `src/render/` code
1110
+ * @override pressureIterations, viscosity, vorticity, and splatRadius tune the solver without changing its pass order
1111
+ * @example const field = new FluidField2D({ resolution: 256, viscosity: 0, pressureIterations: 20 });
1112
+ * ctx.add(field);
1113
+ * field.splat({ x: 0.5, y: 0.5 }, { x: 0.2, y: 0 }, 1);
1114
+ */
1115
+ declare class FluidField2D extends Group {
1116
+ #private;
1117
+ readonly resolution: number;
1118
+ readonly viscosity: number;
1119
+ readonly pressureIterations: number;
1120
+ readonly maxSplats: number;
1121
+ readonly timeStep: number;
1122
+ readonly vorticity: number;
1123
+ readonly splatRadius: number;
1124
+ readonly processCadence: "fixed";
1125
+ readonly warmupNodes: readonly ComputeNode[];
1126
+ readonly velocity: IFluidFieldSampler;
1127
+ readonly dye: IFluidFieldSampler;
1128
+ constructor(options: IFluidFieldOptions);
1129
+ get released(): boolean;
1130
+ get queuedSplats(): number;
1131
+ get steps(): number;
1132
+ get splatsApplied(): number;
1133
+ attachRenderer(renderer: IRendererLike): void;
1134
+ splat(uv: IFluidFieldVector2, velocity: IFluidFieldVector2, amount: number): void;
1135
+ process(renderer?: IRendererLike | undefined): void;
1136
+ detach(): void;
1137
+ }
1138
+
1139
+ type WaveDirection = readonly [number, number] | {
1140
+ readonly x: number;
1141
+ readonly z?: number;
1142
+ readonly y?: number;
1143
+ };
1144
+ interface IWaveFieldWave {
1145
+ readonly amplitude?: number;
1146
+ readonly direction: WaveDirection;
1147
+ readonly wavelength: number;
1148
+ readonly speed: number;
1149
+ readonly phase?: number;
1150
+ readonly steepness?: number;
1151
+ }
1152
+ interface IWaveFieldDomainWarp {
1153
+ readonly direction?: WaveDirection;
1154
+ readonly waveVector?: WaveDirection;
1155
+ readonly displacement?: WaveDirection;
1156
+ readonly amplitude?: number;
1157
+ readonly wavelength?: number;
1158
+ readonly speed: number;
1159
+ readonly phase?: number;
1160
+ }
1161
+ interface IWaveFieldOptions {
1162
+ readonly waves: readonly IWaveFieldWave[];
1163
+ readonly domainWarp?: readonly IWaveFieldDomainWarp[];
1164
+ }
1165
+ interface IWaveFieldSample {
1166
+ readonly height: number;
1167
+ readonly normal: Vector3;
1168
+ }
1169
+ /**
1170
+ * An analytic wave field with one packed parameter source for CPU sampling and TSL displacement.
1171
+ * It owns no geometry or appearance; a game chooses how the returned displacement is drawn.
1172
+ */
1173
+ declare class WaveField {
1174
+ #private;
1175
+ readonly waves: readonly IWaveFieldWave[];
1176
+ readonly domainWarp: readonly IWaveFieldDomainWarp[];
1177
+ readonly parameters: Readonly<Float32Array>;
1178
+ readonly time: three_webgpu.UniformNode<"float", number>;
1179
+ constructor(options: IWaveFieldOptions);
1180
+ /** Update the default graph clock. Explicit sample times remain available for fixed-step code. */
1181
+ setTime(value: number): void;
1182
+ sample(x: number, z: number, time: number): IWaveFieldSample;
1183
+ /** Return a TSL node that displaces local vertices using the same packed values as `sample`. */
1184
+ displacementNode(timeNode?: three_webgpu.UniformNode<"float", number>): three_webgpu.Node<"vec3">;
1185
+ }
1186
+
1187
+ /**
1188
+ * A soft round sprite, built as pixel data rather than by painting a canvas.
1189
+ *
1190
+ * Canvas-drawn images sample black under `WebGPURenderer` — a documented trap that cost a shipped
1191
+ * game real debugging time — so sprite images are written straight into pixel data. A radial alpha
1192
+ * falloff is also the whole difference between a puff and a rectangle: a flat quad reads as a grey
1193
+ * box, the same quad with this alpha reads as smoke.
1194
+ *
1195
+ * @param size edge length in pixels
1196
+ * @param hardness 0 fades from the very centre, 1 keeps a solid core out to the rim
1197
+ */
1198
+ declare function softCircleDataTexture(size?: number, hardness?: number): DataTexture;
1199
+
1200
+ interface ITracerPool3DOptions {
1201
+ /** Slots in the pool. Shots over the count recycle the oldest streak. Default 12. */
1202
+ readonly count?: number;
1203
+ /**
1204
+ * The streak's shape, supplied by the game. The default is a neutral unit-length cylinder
1205
+ * along +Y with its base at the origin — the pool stretches it along y, so any geometry laid
1206
+ * out the same way works. Override it to change the streak's cross-section or silhouette.
1207
+ */
1208
+ readonly geometry?: BufferGeometry;
1209
+ /**
1210
+ * The streak's surface, supplied by the game and cloned per slot so each can fade
1211
+ * independently. Required: pooling, travel and fading are the engine's; what the streak
1212
+ * looks like never is. Set `opacity` to the peak brightness and pass a transparent,
1213
+ * additive surface for the usual bright-fade look.
1214
+ */
1215
+ readonly material: Material;
1216
+ /** Longest streak in metres; shorter shots get a shorter streak. Default 3.2. */
1217
+ readonly segmentLength?: number;
1218
+ /** Travel speed in metres per second. Default 360. */
1219
+ readonly speed?: number;
1220
+ /** Seconds a streak lives before fading out fully. Default 0.11. */
1221
+ readonly lifetime?: number;
1222
+ }
1223
+ /**
1224
+ * Per-shot overrides a game passes to {@link TracerPool3D.spawn} — shot-to-shot variation
1225
+ * keeps two rounds from reading as one drawn line. The values are the game's (usually from
1226
+ * its seeded random so replays stay identical); the pool only applies them.
1227
+ */
1228
+ interface ITracerSpawnOptions {
1229
+ /** Longest streak for this shot, in metres. Defaults to the pool's `segmentLength`. */
1230
+ readonly segmentLength?: number;
1231
+ /** Seconds this streak lives before fading out fully. Defaults to the pool's `lifetime`. */
1232
+ readonly lifetime?: number;
1233
+ /** Multiplier on this streak's cross-section (x/z scale). Default 1. */
1234
+ readonly widthScale?: number;
1235
+ }
1236
+ /**
1237
+ * Pooled travelling bullet streaks for hitscan shots.
1238
+ *
1239
+ * A hitscan round leaves nothing to see, so a shot is only a sound and a number — you cannot tell
1240
+ * where it went or who is firing. The pool draws a short bright segment that travels from the
1241
+ * muzzle toward the point reached and fades out. Segments are stretched cylinders rather than
1242
+ * `Line`s, because line width is not portable across backends and a one-pixel line is invisible at
1243
+ * thirty metres.
1244
+ *
1245
+ * Every member starts visible at zero opacity, so the whole pool doubles as a pipeline prewarm
1246
+ * surface (`prewarm(tracers)`); nothing is created while firing. Call {@link update} once per
1247
+ * frame and {@link dispose} with the owning scene.
1248
+ */
1249
+ declare class TracerPool3D {
1250
+ #private;
1251
+ constructor(parent: Object3D, options: ITracerPool3DOptions);
1252
+ /**
1253
+ * Stop submitting the slots that are not carrying a shot.
1254
+ *
1255
+ * The pool is resident from construction at zero opacity so its pipeline compiles during
1256
+ * loading; the cost is a draw per dead streak every frame forever, and on a phone the draw call
1257
+ * is the expensive part, not the triangles. Once compiled the pipeline is cached, so `spawn`
1258
+ * re-showing a slot is free. Call this a second or two into the scene, after `prewarm` — not on
1259
+ * the first frame, or the compile this exists to force will not have happened yet.
1260
+ */
1261
+ settle(): void;
1262
+ /**
1263
+ * Draw one round travelling from `from` along `direction` for `distance` metres.
1264
+ * `options` carries the game's per-shot variation; omit it for the pool defaults.
1265
+ */
1266
+ spawn(from: Vector3, direction: Vector3, distance: number, options?: ITracerSpawnOptions): void;
1267
+ /** Advance every live streak; call once per frame with the frame's delta seconds. */
1268
+ update(dt: number): void;
1269
+ /** Remove every mesh from the parent and release pooled surfaces. Game-owned geometry survives. */
1270
+ dispose(): void;
1271
+ }
1272
+
1273
+ interface IPathFollow3DOptions {
1274
+ readonly loop?: boolean;
1275
+ readonly points: readonly Vector3[];
1276
+ readonly speed?: number;
1277
+ }
1278
+ interface IPathFollow3DSample {
1279
+ point: Vector3;
1280
+ progress: number;
1281
+ tangent: Vector3;
1282
+ }
1283
+ interface IPathFollow3DProjection {
1284
+ distanceFromStart: number;
1285
+ lateralDistance: number;
1286
+ tangent: Vector3;
1287
+ point: Vector3;
1288
+ segment: number;
1289
+ }
1290
+ /** A portable, distance-based follower for an authored Three.js route. */
1291
+ declare class PathFollow3D {
1292
+ #private;
1293
+ readonly curve: CatmullRomCurve3;
1294
+ readonly loop: boolean;
1295
+ readonly totalLength: number;
1296
+ constructor(options: IPathFollow3DOptions);
1297
+ get completed(): boolean;
1298
+ get progress(): number;
1299
+ get speed(): number;
1300
+ set speed(value: number);
1301
+ advance(dt: number, target?: IPathFollow3DSample): IPathFollow3DSample;
1302
+ progressTo(distance: number): this;
1303
+ sample(distance?: number, target?: IPathFollow3DSample): IPathFollow3DSample;
1304
+ pointAt(distance: number, target?: IPathFollow3DSample): IPathFollow3DSample;
1305
+ project(position: Vector3, target?: IPathFollow3DProjection): IPathFollow3DProjection;
1306
+ }
1307
+
1308
+ interface IGroundSnapOptions {
1309
+ /** Whether to apply the correction. Measurement and `clearance` continue when this is false. */
1310
+ readonly enabled?: boolean;
1311
+ /** Maximum correction speed in metres per second. Unset follows the authored pose exactly. */
1312
+ readonly maxRate?: number;
1313
+ /** Visual meshes to measure. Defaults to every mesh below `model`. */
1314
+ readonly meshes?: readonly Object3D[];
1315
+ }
1316
+ /**
1317
+ * Keeps the lowest posed point of a rendered model on a surface.
1318
+ *
1319
+ * This is render grounding, not collider snap-to-ground. It uses a cached skin envelope so a
1320
+ * frame loop never runs the precise per-vertex bounds path. `enabled` is deliberately a range:
1321
+ * turning correction off still leaves `clearance` and `audit()` truthful.
1322
+ */
1323
+ declare class GroundSnap {
1324
+ readonly model: Object3D;
1325
+ readonly meshes: readonly Object3D[] | undefined;
1326
+ enabled: boolean;
1327
+ maxRate: number | undefined;
1328
+ clearance: number | null;
1329
+ private readonly parentInverse;
1330
+ private readonly parentOrigin;
1331
+ private readonly parentTarget;
1332
+ constructor(model: Object3D, options?: IGroundSnapOptions);
1333
+ /** Move `group` so its lowest posed point meets `surfaceY`, then report the real clearance. */
1334
+ apply(group: Object3D, surfaceY: number, dt: number): void;
1335
+ private applyWorldCorrection;
1336
+ /**
1337
+ * Compare the cheap envelope's lower bound with a precise vertex measurement.
1338
+ *
1339
+ * This is intentionally opt-in: calling it in `apply()` would restore the frame-time defect
1340
+ * this class exists to remove. A negative result means the envelope is below the precise skin.
1341
+ */
1342
+ audit(): number | null;
1343
+ }
1344
+
1345
+ type ThreePoseVector = readonly [number, number, number];
1346
+ type ThreePoseQuaternion = readonly [number, number, number, number];
1347
+ interface IThreePoseBounds {
1348
+ readonly min: ThreePoseVector;
1349
+ readonly max: ThreePoseVector;
1350
+ readonly size: ThreePoseVector;
1351
+ }
1352
+ /** JSON-safe world-space measurements for one Three.js object and its visual bounds. */
1353
+ interface IThreePoseMeasurement {
1354
+ readonly name: string;
1355
+ readonly type: string;
1356
+ readonly position: ThreePoseVector;
1357
+ readonly quaternion: ThreePoseQuaternion;
1358
+ readonly scale: ThreePoseVector;
1359
+ readonly axes: {
1360
+ readonly x: ThreePoseVector;
1361
+ readonly y: ThreePoseVector;
1362
+ readonly z: ThreePoseVector;
1363
+ };
1364
+ readonly bounds: IThreePoseBounds | null;
1365
+ }
1366
+ interface IMeasureThreePoseOptions {
1367
+ /**
1368
+ * Objects whose geometry forms the reported bounds. Defaults to `object`.
1369
+ * This path is precise; it walks every vertex. Do not call it in a frame loop — see
1370
+ * `posedBounds`.
1371
+ */
1372
+ readonly bounds?: readonly Object3D[] | false;
1373
+ }
1374
+ /**
1375
+ * Measure an Object3D in world space for attachment and animation diagnostics.
1376
+ *
1377
+ * Passing explicit `bounds` lets a probe measure a body without an attached weapon or
1378
+ * invisible hitbox. The result is JSON-safe so it can cross the browser playtest bridge.
1379
+ */
1380
+ declare function measureThreePose(object: Object3D, options?: IMeasureThreePoseOptions): IThreePoseMeasurement;
1381
+ /**
1382
+ * Cheap world-space bounds for a posed model.
1383
+ *
1384
+ * The first call pays the precise vertex walk to build a conservative skin envelope. Later calls
1385
+ * read one world-matrix translation per contributing bone and allocate nothing. The returned
1386
+ * object is cached for the root and updated in place; copy it if it must outlive the next call.
1387
+ */
1388
+ declare function posedBounds(root: Object3D, meshes?: readonly Object3D[]): IThreePoseBounds;
1389
+
1390
+ type PlatformRuntime = "web" | "native";
1391
+ type PlatformOS = "android" | "ios" | "linux" | "macos" | "windows" | "unknown";
1392
+ type PlatformFormFactor = "mobile" | "desktop" | "unknown";
1393
+ interface IPlatformInfo {
1394
+ readonly runtime: PlatformRuntime;
1395
+ readonly os: PlatformOS;
1396
+ readonly formFactor: PlatformFormFactor;
1397
+ readonly maxTouchPoints: number;
1398
+ }
1399
+ declare function getPlatform(): Readonly<IPlatformInfo>;
1400
+ declare function isWeb(): boolean;
1401
+ declare function isNative(): boolean;
1402
+ declare function isMobile(): boolean;
1403
+ declare function isTouchscreenAvailable(): boolean;
1404
+
1405
+ type ReplayPointer = readonly [number, number, number, number, number];
1406
+ interface IReplayRecordingSample {
1407
+ readonly keys: readonly string[];
1408
+ readonly pointer?: ReplayPointer;
1409
+ readonly tick: number;
1410
+ }
1411
+ interface IReplayRecording {
1412
+ readonly input: readonly IReplayRecordingSample[];
1413
+ readonly randomState: number;
1414
+ readonly runtime: {
1415
+ agent: string;
1416
+ core: string;
1417
+ portable?: boolean;
1418
+ rapier: string | null;
1419
+ step: number;
1420
+ };
1421
+ readonly seed: number;
1422
+ readonly ticks: number;
1423
+ readonly version: 1;
1424
+ }
1425
+ declare function parseReplayRecording(value: unknown): IReplayRecording;
1426
+
72
1427
  type Recording = IReplayRecording;
1428
+ interface IReplayOptions {
1429
+ /** Allow a recording to be replayed by a different host with the same simulation contract. */
1430
+ readonly portable?: boolean;
1431
+ }
73
1432
  type ReplayPublic = {
74
1433
  readonly recording: Recording | undefined;
75
1434
  readonly runId: symbol;
76
1435
  };
77
- declare function replay<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined>(): GamePluginHooks<TState, TPhysics> & ReplayPublic;
78
- declare function createReplayDriver(recording: Recording, target: EventTarget, pointerTarget?: EventTarget): ((runtime: GamePluginRuntime) => number) & {
79
- prepare: (runtime: GamePluginRuntime) => void;
1436
+ declare function replay<TState extends Record<string, unknown> = Record<string, unknown>, TPhysics = undefined>(options?: IReplayOptions): IGamePluginHooks<TState, TPhysics> & ReplayPublic;
1437
+ declare function createReplayDriver(recording: Recording, target: EventTarget, pointerTarget?: EventTarget): ((runtime: IGamePluginRuntime) => number) & {
1438
+ prepare: (runtime: IGamePluginRuntime) => void;
80
1439
  runId: symbol;
81
1440
  };
82
1441
 
83
- declare const version = "0.1.0";
1442
+ type SpritePlaybackMode = "loop" | "pingPong" | "once";
1443
+ interface ISpriteFrame3D {
1444
+ /** Pixels from the left edge of the atlas. */
1445
+ readonly x: number;
1446
+ /** Pixels from the top edge when `origin` is `"top-left"`. */
1447
+ readonly y: number;
1448
+ readonly width: number;
1449
+ readonly height: number;
1450
+ /** Seconds this frame is held; every frame must provide its own authored timing. */
1451
+ readonly duration: number;
1452
+ }
1453
+ interface ISpriteAnimator3DOptions {
1454
+ /** The game-owned atlas texture. Its filters, wrapping and surface remain untouched. */
1455
+ readonly texture: Texture;
1456
+ /** Pixel-space atlas rectangles with per-frame durations. */
1457
+ readonly frames: readonly ISpriteFrame3D[];
1458
+ readonly mode?: SpritePlaybackMode;
1459
+ /** Atlas coordinate origin; top-left is conventional for exported sprite sheets. */
1460
+ readonly origin?: "top-left" | "bottom-left";
1461
+ /** Start advancing immediately unless the game explicitly opts out. */
1462
+ readonly autoPlay?: boolean;
1463
+ }
1464
+ /**
1465
+ * Advance a game-owned atlas texture on the fixed step supplied by its owner.
1466
+ *
1467
+ * This helper changes only `texture.offset` and `texture.repeat`. The game still chooses the
1468
+ * texture, filters, wrapping, surface, geometry and every frame's duration. Call {@link update}
1469
+ * from the scene's fixed-step update; no wall clock or global animation loop is consulted.
1470
+ */
1471
+ declare class SpriteAnimator3D {
1472
+ #private;
1473
+ readonly texture: Texture;
1474
+ readonly frames: readonly ISpriteFrame3D[];
1475
+ readonly mode: SpritePlaybackMode;
1476
+ readonly origin: "top-left" | "bottom-left";
1477
+ constructor(options: ISpriteAnimator3DOptions);
1478
+ get frameIndex(): number;
1479
+ get elapsed(): number;
1480
+ get finished(): boolean;
1481
+ get playing(): boolean;
1482
+ /** Advance by one caller-supplied fixed-step delta. */
1483
+ update(dt: number): this;
1484
+ /** Pause fixed-step advancement while leaving the selected frame visible. */
1485
+ pause(): this;
1486
+ /** Resume, restarting a completed one-shot from frame zero. */
1487
+ play(): this;
1488
+ /** Stop and reset to the first authored frame. */
1489
+ stop(): this;
1490
+ /** Select a frame without advancing time. */
1491
+ setFrame(index: number): this;
1492
+ }
1493
+
1494
+ type NormaliseAxis = "height" | "longest";
1495
+ interface INormaliseToMetresOptions {
1496
+ readonly metres: number;
1497
+ readonly axis: NormaliseAxis;
1498
+ /** Crown bone to use for a skinned height measurement. Defaults to a named/highest bone. */
1499
+ readonly top?: Object3D | string;
1500
+ }
1501
+ /**
1502
+ * Scale an asset to a real-world size and return the factor applied.
1503
+ *
1504
+ * Height for a skinned asset comes from its crown bone, not its bind-pose Box3. Longest-axis
1505
+ * normalization remains a geometry measurement because it is used for rigid props and weapons.
1506
+ */
1507
+ declare function normaliseToMetres(object: Object3D, options: INormaliseToMetresOptions): number;
1508
+
1509
+ /** Names of every bone in traversal order. Empty for an unskinned model. */
1510
+ declare function skeletonBones(root: Object3D): readonly string[];
1511
+ /** Parent a child to a named bone while preserving the child's authored world scale. */
1512
+ declare function attachToBone(root: Object3D, boneName: string, child: Object3D): Object3D;
1513
+
1514
+ /**
1515
+ * The version this library reports.
1516
+ *
1517
+ * It read `0.1.0` while the package published `0.2.0`, and `__tests__/build.spec.ts` asserted the
1518
+ * stale literal, so the test held the bug in place rather than catching it. A literal is
1519
+ * unavoidable here — core is bundled for browsers and cannot read `package.json` at runtime — so
1520
+ * the spec now asserts this equals the manifest instead of asserting a number somebody typed.
1521
+ */
1522
+ declare const version = "0.3.0";
84
1523
 
85
- export { type AnimationPlayOptions, AnimationPlayer, type AnimationPlayerOptions, FixedStepLoop, type FixedStepLoopOptions, GPUParticles3D, type GPUParticles3DBuffers, type GPUParticles3DOptions, GamePluginHooks, GamePluginRuntime, type Recording, RendererLike, createReplayDriver, replay, version };
1524
+ export { ATLAS_PADDING, ATMOSPHERE_LUT_RESOLUTIONS, AnimationPlayer, Atmosphere, type AtmosphereDirection, AtmosphereLuts, type AtmosphereRgb, Billboard3D, type BillboardLockAxis, CameraShake, type CameraShakeCurve, ClusteredBatch, ClusteredMesh, ComputeDrivenRegistry, FluidField2D, GPUParticles3D, GPUReadback, GPUSceneBVH, type GPUSceneBVHTraceFunction, GroundSnap, type IAnimationPlayOptions, type IAnimationPlayerOptions, type IAtmosphereLutResolution, type IAtmosphereLutResolutions, type IAtmosphereOptions, type IAtmosphereParameterPatch, type IAtmosphereParameters, type IAtmosphereScenePass, type IBillboard3DOptions, type ICameraShakeOffset, type ICameraShakeOptions, type IClusterTable, type IClusteredBatchBuildOptions, type IClusteredBatchOptions, type IClusteredMeshOptions, type IClusteredPlacement, type IComputeDriven, type IFluidFieldOptions, type IFluidFieldSampler, type IFluidFieldVector2, type IGPUReadbackOptions, type IGPUReadbackSample, type IGPUSceneBVHMaterialGroup, type IGPUSceneBVHOptions, IGamePluginHooks, IGamePluginRuntime, type IGroundSnapOptions, type IInstancedBatchBuildOptions, type IInstancedBatchOptions, type IInstancedPlacement, type IMeasureThreePoseOptions, 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 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 IWaveFieldDomainWarp, 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, SoftBody3D, SpectralOcean, SpriteAnimator3D, type SpritePlaybackMode, type ThreePoseQuaternion, type ThreePoseVector, TracerPool3D, type WaveDirection, WaveField, attachToBone, bvhIntersectFirstHit, createReplayDriver, directionFromSolarPosition, directionalTransmittance, getPlatform, isMobile, isNative, isTouchscreenAvailable, isWeb, measureThreePose, normaliseToMetres, parseReplayRecording, posedBounds, rayStruct, readProbeVolumeObservation, replay, resolveAtmosphereLutResolutions, resolveAtmosphereParameters, skeletonBones, softCircleDataTexture, solarPosition, solarPositionAt, updateAtmosphereParameters, updateClusteredMeshes, version, zenithTransmittance };