@voluma/vlam 0.1.0 → 0.2.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/THIRD_PARTY_NOTICES.md +86 -0
  4. package/dist/budget-governor.d.ts +154 -0
  5. package/dist/camera-budget-governor.d.ts +234 -0
  6. package/dist/chunk-cache-budget.d.ts +156 -0
  7. package/dist/chunk-fetch-scheduler.d.ts +168 -0
  8. package/dist/chunk-loader.d.ts +85 -0
  9. package/dist/compute-sorter.d.ts +131 -0
  10. package/dist/create-splat-renderer.d.ts +99 -0
  11. package/dist/dataset-source.d.ts +55 -0
  12. package/dist/depth-of-field.d.ts +73 -0
  13. package/dist/effects.d.ts +337 -0
  14. package/dist/effects.js +180 -0
  15. package/dist/effects.js.map +1 -0
  16. package/dist/formats/ksplat/index.d.ts +8 -0
  17. package/dist/formats/ksplat/parse-ksplat.d.ts +9 -0
  18. package/dist/formats/ksplat.js +167 -0
  19. package/dist/formats/ksplat.js.map +1 -0
  20. package/dist/formats/lcc/collision-mesh.d.ts +37 -0
  21. package/dist/formats/lcc/collision-partition.d.ts +35 -0
  22. package/dist/formats/lcc/index.d.ts +16 -0
  23. package/dist/formats/lcc/lcc.d.ts +28 -0
  24. package/dist/formats/lcc/lcc2-transform.d.ts +18 -0
  25. package/dist/formats/lcc/lcc2.d.ts +4 -0
  26. package/dist/formats/lcc/parse-collision-lci.d.ts +28 -0
  27. package/dist/formats/lcc/parse-lcc.d.ts +177 -0
  28. package/dist/formats/lcc/parse-mesh-ply.d.ts +21 -0
  29. package/dist/formats/lcc.js +969 -0
  30. package/dist/formats/lcc.js.map +1 -0
  31. package/dist/formats/ply/index.d.ts +12 -0
  32. package/dist/formats/ply/parse-compressed-ply.d.ts +16 -0
  33. package/dist/formats/ply/parse-splat-ply.d.ts +46 -0
  34. package/dist/formats/ply.js +230 -0
  35. package/dist/formats/ply.js.map +1 -0
  36. package/dist/formats/rad/frontier-worker-protocol.d.ts +177 -0
  37. package/dist/formats/rad/index.d.ts +10 -0
  38. package/dist/formats/rad/parse-rad.d.ts +88 -0
  39. package/dist/formats/rad/rad-column-decoders.d.ts +15 -0
  40. package/dist/formats/rad/rad-foveated-source.d.ts +71 -0
  41. package/dist/formats/rad/rad-parent-sizes.d.ts +45 -0
  42. package/dist/formats/rad/rad.d.ts +132 -0
  43. package/dist/formats/rad.js +1098 -0
  44. package/dist/formats/rad.js.map +1 -0
  45. package/dist/formats/sog/index.d.ts +9 -0
  46. package/dist/formats/sog/parse-sog.d.ts +45 -0
  47. package/dist/formats/sog.js +257 -0
  48. package/dist/formats/sog.js.map +1 -0
  49. package/dist/formats/splat/index.d.ts +8 -0
  50. package/dist/formats/splat/parse-splat.d.ts +10 -0
  51. package/dist/formats/splat.js +39 -0
  52. package/dist/formats/splat.js.map +1 -0
  53. package/dist/formats/spz/index.d.ts +8 -0
  54. package/dist/formats/spz/parse-spz.d.ts +10 -0
  55. package/dist/formats/spz.js +218 -0
  56. package/dist/formats/spz.js.map +1 -0
  57. package/dist/frontier-worker-CAnK_Cmu.js +566 -0
  58. package/dist/frontier-worker-CAnK_Cmu.js.map +1 -0
  59. package/dist/half-float-Cs4FDq8Q.js +30 -0
  60. package/dist/half-float-Cs4FDq8Q.js.map +1 -0
  61. package/dist/half-float.d.ts +15 -0
  62. package/dist/index.d.ts +54 -0
  63. package/dist/index.js +9054 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/load-scene.d.ts +50 -0
  66. package/dist/load-worker-protocol.d.ts +82 -0
  67. package/dist/loading-wLo8vRbA.js +102 -0
  68. package/dist/loading-wLo8vRbA.js.map +1 -0
  69. package/dist/loading.d.ts +150 -0
  70. package/dist/lod-manifest.d.ts +61 -0
  71. package/dist/lod-scheduler.d.ts +229 -0
  72. package/dist/lod-source.d.ts +207 -0
  73. package/dist/logging-BfPdd7NJ.js +19 -0
  74. package/dist/logging-BfPdd7NJ.js.map +1 -0
  75. package/dist/logging.d.ts +37 -0
  76. package/dist/one-shot-worker-G80btnth.js +664 -0
  77. package/dist/one-shot-worker-G80btnth.js.map +1 -0
  78. package/dist/orientation.d.ts +35 -0
  79. package/dist/ply-header-BwgExCn-.js +125 -0
  80. package/dist/ply-header-BwgExCn-.js.map +1 -0
  81. package/dist/ply-header.d.ts +87 -0
  82. package/dist/radix-sort.d.ts +28 -0
  83. package/dist/radix-sorter.d.ts +46 -0
  84. package/dist/relighting.d.ts +50 -0
  85. package/dist/selection-volume.d.ts +87 -0
  86. package/dist/sh-pack-D5wAe5gg.js +54 -0
  87. package/dist/sh-pack-D5wAe5gg.js.map +1 -0
  88. package/dist/sh-pack.d.ts +49 -0
  89. package/dist/sort-scheduler.d.ts +62 -0
  90. package/dist/sort-worker-protocol.d.ts +37 -0
  91. package/dist/sorter.d.ts +31 -0
  92. package/dist/source-transform.d.ts +57 -0
  93. package/dist/splat-budget-PSojLJPO.js +157 -0
  94. package/dist/splat-budget-PSojLJPO.js.map +1 -0
  95. package/dist/splat-budget.d.ts +404 -0
  96. package/dist/splat-data-BZM5dhhx.js +10 -0
  97. package/dist/splat-data-BZM5dhhx.js.map +1 -0
  98. package/dist/splat-data.d.ts +150 -0
  99. package/dist/splat-depth-pack.d.ts +39 -0
  100. package/dist/splat-mesh-material.d.ts +255 -0
  101. package/dist/splat-mesh-picking.d.ts +87 -0
  102. package/dist/splat-mesh-pool.d.ts +241 -0
  103. package/dist/splat-mesh-types-8QDjTbZO.js +390 -0
  104. package/dist/splat-mesh-types-8QDjTbZO.js.map +1 -0
  105. package/dist/splat-mesh-types.d.ts +390 -0
  106. package/dist/splat-mesh.d.ts +737 -0
  107. package/dist/splat-modifier-stack.d.ts +38 -0
  108. package/dist/splat-modifier.d.ts +156 -0
  109. package/dist/splat-partition.d.ts +37 -0
  110. package/dist/splat-query.d.ts +51 -0
  111. package/dist/splat-scene.d.ts +127 -0
  112. package/dist/splat-sort-bounds.d.ts +11 -0
  113. package/dist/static-lod-splat-mesh.d.ts +53 -0
  114. package/dist/static-lod-worker-D2fyqQZO.js +474 -0
  115. package/dist/static-lod-worker-D2fyqQZO.js.map +1 -0
  116. package/dist/static-lod-worker-protocol.d.ts +37 -0
  117. package/dist/static-lod.d.ts +24 -0
  118. package/dist/storage-attribute-mirror.d.ts +103 -0
  119. package/dist/streamed-splat-mesh-utils.d.ts +146 -0
  120. package/dist/streamed-splat-mesh.d.ts +985 -0
  121. package/dist/unified-splat-renderer.d.ts +205 -0
  122. package/dist/webgpu-limits.d.ts +133 -0
  123. package/dist/work-buffer-gather.d.ts +119 -0
  124. package/dist/work-buffer-material.d.ts +42 -0
  125. package/dist/worker-sorter.d.ts +57 -0
  126. package/dist/xr-view.d.ts +92 -0
  127. package/package.json +135 -21
@@ -0,0 +1,55 @@
1
+ import { type SplatRequestOptions, type StreamedSplatFormat } from './loading';
2
+ /**
3
+ * Where a streamed dataset's files come from.
4
+ *
5
+ * A streamed scene is never one file: it is a manifest plus sidecars and chunk
6
+ * files that the manifest names *relatively* (`data.bin`, `data/3dgs/x.sog`,
7
+ * `0_0/meta.json`). Over HTTP those resolve against the manifest's URL; from a
8
+ * dropped folder there is no URL to resolve against, only `File` objects.
9
+ *
10
+ * This is the seam between the two. Everything downstream - including the
11
+ * worker's ranged chunk reads - keeps working on plain URLs, because a
12
+ * dropped file's `blob:` URL answers `Range` with a real 206 (verified on
13
+ * Chromium/WebKit/Gecko), so only *name resolution* has to differ.
14
+ */
15
+ export interface SplatDatasetSource {
16
+ /** Fetchable URL of the manifest itself. */
17
+ readonly manifestUrl: string;
18
+ /** Fetchable URL for a dataset-relative path, or null when absent. */
19
+ resolve(path: string): string | null;
20
+ /** Byte length of a file, or null when absent. */
21
+ size(path: string): Promise<number | null>;
22
+ /**
23
+ * The files inside a chunk *directory* (unbundled SOG), as
24
+ * `name → fetchable URL`. Null when the chunk should be fetched by URL
25
+ * instead - the HTTP case, where the directory is a real path.
26
+ */
27
+ directoryFiles(path: string): Record<string, string> | null;
28
+ /** Releases any resources held for this dataset (e.g. blob URLs). */
29
+ dispose(): void;
30
+ }
31
+ /** A dataset served over HTTP, resolved against the manifest's URL. */
32
+ export declare function httpDatasetSource(manifestUrl: string, request?: SplatRequestOptions): SplatDatasetSource;
33
+ /** A dataset picked out of a dropped folder. */
34
+ export interface LocalDataset {
35
+ readonly source: SplatDatasetSource;
36
+ /** The streamed format the folder's manifest identifies it as. */
37
+ readonly format: Exclude<StreamedSplatFormat, 'auto'>;
38
+ /** The manifest's file name, for display. */
39
+ readonly name: string;
40
+ }
41
+ /**
42
+ * Builds a dataset from a dropped folder's files, keyed by their paths
43
+ * relative to the folder root.
44
+ *
45
+ * Every file gets a `blob:` URL, so the rest of the pipeline - including the
46
+ * worker's ranged reads into a multi-hundred-megabyte `data.bin` - is
47
+ * identical to the HTTP path. Nothing is copied or read here: a blob URL is a
48
+ * handle to the file on disk, so a 300 MB `data.bin` costs nothing until a
49
+ * chunk actually reads a range out of it.
50
+ *
51
+ * @throws {SplatLoadError} with `phase: 'manifest'` when the folder holds no
52
+ * recognizable manifest, or more than one (which would make the choice
53
+ * arbitrary). Neither is retryable: the same folder fails the same way.
54
+ */
55
+ export declare function createLocalDataset(files: ReadonlyMap<string, File>): LocalDataset;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Core projected-2D depth of field (post-EWA isotropic CoC).
3
+ *
4
+ * Spark-style pinhole CoC:
5
+ * focusBlur = |depth − focus| / depth
6
+ * apertureRadius = focalPx · tan(0.5 · apertureAngle)
7
+ * cocRadius = focusBlur · apertureRadius
8
+ * blurVariance = clamp(cocRadius², …, maxRadius²)
9
+ *
10
+ * `/ depth` is Spark's exact falloff (`splatVertex.glsl`), restored for visual
11
+ * parity: scenes authored against Spark's foreground defocus rendered flat here
12
+ * under the previous `/ max(depth, focus)`, which capped focusBlur at 1 and so
13
+ * limited the CoC to a single apertureRadius - sub-pixel at the aperture sizes
14
+ * ModifyCamera produces. Behind the focus plane the two forms are identical
15
+ * (`max(depth, focus) === depth`), so only the near side changes.
16
+ *
17
+ * Note `/ depth` is unbounded as depth → 0. The fill-rate guard is the same one
18
+ * Spark uses: {@link MAX_DOF_RADIUS_PX} clamps the CoC. Do not "fix" the near
19
+ * side by dividing by focus instead - that pushes every *background* splat to
20
+ * the cap when zooming out with a near focus plane (huge quads → fill-rate
21
+ * death), which is what the bounded form was originally guarding against.
22
+ *
23
+ * Voluma's ModifyCamera `apertureSize` maps to an angle through the *live*
24
+ * focus distance - `apertureAngle = 2·atan(0.5·size / focus)` - matching the
25
+ * host helper Voluma ships for Spark. A fixed reference distance was tried
26
+ * instead to stop near focus planes from
27
+ * ballooning every splat, but it decouples aperture from focus and so flattens
28
+ * the intended effect: racking focus toward the camera is exactly how authors
29
+ * get the whole scene to go soft. Keep the mapping focus-relative; the CoC cap
30
+ * is what bounds fill-rate.
31
+ *
32
+ * Unlike the M13 `depthOfFieldPreset` modifier (3D scale), this path adds
33
+ * `coc²·I` to the projected 2D covariance with `√(det)` opacity fade.
34
+ */
35
+ /**
36
+ * Soft CoC radius cap in pixels (Spark clamps with `maxPixelRadius`).
37
+ * Kept modest for interactive fill-rate; raise only if cinematic stills need it.
38
+ */
39
+ export declare const MAX_DOF_RADIUS_PX = 24;
40
+ /** Cap on CoC variance (px²) added to the projected 2D covariance diagonal. */
41
+ export declare const MAX_DOF_VARIANCE: number;
42
+ /** Live camera depth-of-field settings for {@link SplatMesh.setDepthOfField}. */
43
+ export type DepthOfFieldSettings = {
44
+ /** View-space distance to the focal plane (world units). Must stay positive. */
45
+ focusDistance: number;
46
+ /** Aperture size in the same units as Voluma `DofSettings.apertureSize`. `0` = off. */
47
+ aperture: number;
48
+ };
49
+ /**
50
+ * Maps Voluma aperture size → full aperture angle (radians) through the focus
51
+ * distance, identical to the host helper Voluma ships for Spark:
52
+ * `2·atan(0.5·size / focus)`. Pulling focus toward the camera widens the angle,
53
+ * which is what makes a near focus plane soften the whole scene.
54
+ */
55
+ export declare function apertureAngleFromSize(aperture: number, focusDistance: number): number;
56
+ /**
57
+ * Circle-of-confusion variance in px² for a splat at view-space depth.
58
+ * Returns `0` when aperture is disabled. Mirrors Spark's vertex DoF branch.
59
+ */
60
+ export declare function computeDofCocVariancePx2(options: {
61
+ depth: number;
62
+ focusDistance: number;
63
+ aperture: number;
64
+ focalPx: number;
65
+ maxVariance?: number;
66
+ }): number;
67
+ /**
68
+ * Opacity fade that conserves Gaussian mass after isotropic screen-space
69
+ * dilation: `√(detRaw / detBlur)`.
70
+ */
71
+ export declare function computeDofOpacityFade(detRaw: number, detBlur: number): number;
72
+ /** Clamps host-supplied DoF settings before writing live uniforms. */
73
+ export declare function clampDepthOfFieldSettings(settings: Partial<DepthOfFieldSettings>, previous?: DepthOfFieldSettings): DepthOfFieldSettings;
@@ -0,0 +1,337 @@
1
+ /**
2
+ * `@voluma/vlam/effects` - optional, tree-shakeable effects built on the M7
3
+ * {@link SplatModifier} contract. Import only what you use; nothing here is
4
+ * pulled into the core renderer bundle.
5
+ *
6
+ * - {@link sdfEffects}: a declarative sphere/box/cylinder shape list evaluated
7
+ * in-shader from a bounded uniform array (a fixed unroll over `maxShapes`
8
+ * slots, gated by the live count) - add/move/remove shapes is data-only
9
+ * (no pipeline recompile), on WebGPU and the WebGL2 fallback alike (M7.4).
10
+ * The kinds mirror the CPU selection volumes (`createSelectionVolume`), so
11
+ * a host can highlight exactly what a selection would take.
12
+ * - {@link lightingPreset} / {@link revealPreset}: small reference presets,
13
+ * starting points rather than a framework (M7.5).
14
+ * - {@link createRelightingProxy}: builds a gray lit mesh group from collision
15
+ * tiles (or raw geometries) for PlayCanvas-style {@link SplatMesh.setRelighting}.
16
+ * - {@link createRelightingShadowFactorMaterial}: writes a shadow **multiplier**
17
+ * (1 = lit, &lt;1 = umbra) so coverage does not statically tint splats.
18
+ * - {@link worldWarpPreset}: camera-centered sphere wrap (planet / bowl).
19
+ * - {@link depthOfFieldPreset}: stylized modifier-based depth-of-field (M13).
20
+ * For physically-modeled camera DoF prefer the core
21
+ * {@link SplatMesh.setDepthOfField} / `UnifiedSplatRenderer.setDepthOfField`
22
+ * path - this preset is the soft/approximate alternative built purely on
23
+ * the modifier hook.
24
+ *
25
+ * Every preset here reads positions from `ctx.localCenter` - mesh-local space,
26
+ * which inside a `SplatScene` is the splat's **placed** position. So a shape or
27
+ * a reveal spans every source as one scene instead of travelling with a moved
28
+ * one; see `docs/guide/effects-and-modifiers.md` for what that looks like, and
29
+ * `ctx.sourceCenter` for the opposite behaviour.
30
+ *
31
+ * See `docs/guide/effects-and-modifiers.md`.
32
+ */
33
+ import * as THREE from 'three/webgpu';
34
+ import type { SplatModifier } from './splat-modifier';
35
+ import type { CollisionMeshTile } from './formats/lcc/collision-mesh';
36
+ /**
37
+ * A signed-distance primitive kind. `cylinder` is capped and runs along the
38
+ * shape-local Y axis (rotate it with {@link SdfShape.rotation}).
39
+ */
40
+ export type SdfShapeKind = 'sphere' | 'box' | 'cylinder';
41
+ /**
42
+ * What a shape does to the splats it covers:
43
+ * - `tint` - blend the splat color toward the shape color.
44
+ * - `desaturate` - blend the splat color toward its luminance.
45
+ * - `hide` - fade the splat out (soft cutaway).
46
+ * - `rim` - tint only a band around the shape's surface.
47
+ */
48
+ export type SdfMode = 'tint' | 'desaturate' | 'hide' | 'rim';
49
+ /** One SDF shape in a {@link sdfEffects} list. All spaces are mesh-local. */
50
+ export interface SdfShape {
51
+ kind: SdfShapeKind;
52
+ /** Shape center, mesh-local. Default `[0, 0, 0]`. */
53
+ center?: readonly [number, number, number];
54
+ /**
55
+ * Sphere/cylinder radius. Required for `kind: 'sphere'` and
56
+ * `kind: 'cylinder'`; must be positive - {@link SdfEffect.setShapes} throws
57
+ * otherwise.
58
+ */
59
+ radius?: number;
60
+ /**
61
+ * Cylinder full height along the shape-local Y axis. Required (and only
62
+ * used) for `kind: 'cylinder'`; must be positive - {@link SdfEffect.setShapes}
63
+ * throws otherwise.
64
+ */
65
+ height?: number;
66
+ /**
67
+ * Box half-extents. Required (and only used) for `kind: 'box'`; every
68
+ * component must be positive - {@link SdfEffect.setShapes} throws otherwise.
69
+ */
70
+ halfExtents?: readonly [number, number, number];
71
+ /** Orientation quaternion `[x, y, z, w]` (boxes and cylinders). Default identity. */
72
+ rotation?: readonly [number, number, number, number];
73
+ /** Effect color (tint/rim). Default white. */
74
+ color?: readonly [number, number, number];
75
+ /** Soft-edge width in world units; 0 is a hard edge. Default `0`. */
76
+ falloff?: number;
77
+ /** Invert the test - affect the outside of the shape instead. */
78
+ invert?: boolean;
79
+ /** Effect strength in `[0, 1]`. Default `1`. */
80
+ strength?: number;
81
+ mode: SdfMode;
82
+ }
83
+ /** A live SDF effect: one modifier plus a data-only way to update its shapes. */
84
+ export interface SdfEffect {
85
+ /** Attach to `mesh.modifiers`. Built once; updating shapes never recompiles. */
86
+ readonly modifier: SplatModifier;
87
+ /**
88
+ * Replaces the shape list. Purely a uniform-buffer write, so animating
89
+ * shape poses/colors/thresholds every frame costs no pipeline recompile.
90
+ * Shapes beyond {@link maxShapes} are dropped with a one-time warning.
91
+ *
92
+ * Throws (before writing anything) if a sphere or cylinder lacks a positive
93
+ * `radius`, a cylinder lacks a positive `height`, or a box lacks positive
94
+ * `halfExtents` - a zero-size shape is always a caller bug, never a useful
95
+ * no-op.
96
+ */
97
+ setShapes(shapes: readonly SdfShape[]): void;
98
+ /** The compiled-in shape capacity (see {@link sdfEffects} `maxShapes`). */
99
+ readonly maxShapes: number;
100
+ }
101
+ /**
102
+ * Builds a {@link SdfEffect} from a declarative shape list. Shapes live in a
103
+ * bounded uniform array and are tested in-shader against each splat's local
104
+ * center as a **fixed unroll over all `maxShapes` slots**, each slot gated by
105
+ * `slot < count` so unused slots contribute nothing. (An in-shader loop is
106
+ * not viable here - see the TSL varying constraint documented inside the
107
+ * modifier.) Adding, moving or removing a shape - or changing the count - is
108
+ * still a pure uniform write with no recompile. Works on WebGPU and the
109
+ * WebGL2 fallback.
110
+ *
111
+ * `maxShapes` is the compiled capacity (default 32). Because every slot is
112
+ * unrolled into the shader, raising it grows the shader's size and compile
113
+ * time (and the uniform array; very large values can exceed WebGL2's uniform
114
+ * limits), so keep it to what a scene needs.
115
+ *
116
+ * Shape positions are mesh-local. In a `SplatScene` that means scene
117
+ * coordinates - a shape overlapping two sources covers both as one continuous
118
+ * shape - so place them with `computeSplatBounds()`, which reports the sources
119
+ * at their current placement.
120
+ */
121
+ export declare function sdfEffects(initialShapes?: readonly SdfShape[], options?: {
122
+ maxShapes?: number;
123
+ }): SdfEffect;
124
+ /** A preset that exposes a live light direction uniform. */
125
+ export interface LightingPreset {
126
+ readonly modifier: SplatModifier;
127
+ /**
128
+ * Mesh-local light direction; mutate `.value` to animate (no recompile).
129
+ * Inside a `SplatScene` that frame is the scene's, so one direction lights
130
+ * every source consistently however they are posed.
131
+ */
132
+ readonly direction: {
133
+ value: THREE.Vector3;
134
+ };
135
+ }
136
+ /**
137
+ * Simple per-splat Lambertian shading using the splat's approximate normal
138
+ * (`ctx.normal`, the least-variance covariance axis). A starting point for
139
+ * relit product shots - not a lighting rig. Works on both backends.
140
+ *
141
+ * In a `SplatScene` the normal comes from the *placed* covariance, so a source
142
+ * that rotates relights as it turns.
143
+ */
144
+ export declare function lightingPreset(options?: {
145
+ direction?: readonly [number, number, number];
146
+ ambient?: number;
147
+ diffuse?: number;
148
+ }): LightingPreset;
149
+ /** A live depth-of-field preset: focus plane and aperture are mutable uniforms. */
150
+ export interface DepthOfFieldPreset {
151
+ readonly modifier: SplatModifier;
152
+ /** World-unit distance to the focal plane; splats there stay sharp. Mutate
153
+ * `.value` to rack focus (no recompile). Must stay positive. */
154
+ readonly focusDistance: {
155
+ value: number;
156
+ };
157
+ /** Blur strength (aperture-like); 0 is no blur. Mutate `.value`. */
158
+ readonly aperture: {
159
+ value: number;
160
+ };
161
+ }
162
+ /**
163
+ * Depth-of-field on the {@link SplatModifier} hook - **stylized / soft** path
164
+ * (M13). Prefer {@link SplatMesh.setDepthOfField} / the core projected-2D
165
+ * aperture model for camera DoF (isotropic screen-space CoC with √det opacity).
166
+ *
167
+ * A splat's circle of confusion grows with how far its view-space depth
168
+ * sits from `focusDistance`; the modifier blurs it by enlarging it
169
+ * (`scale = 1 + coc`) and, because the material scales the projected covariance
170
+ * by `scale²`, divides opacity by `scale²` so the widened splat conserves its
171
+ * integrated mass instead of over-brightening. Focus and aperture are live
172
+ * uniforms, so racking focus every frame costs no recompile. Pure TSL - works
173
+ * on WebGPU and the WebGL2 fallback.
174
+ *
175
+ * This is an *approximate* DoF: `scale` is a uniform 3D-covariance multiplier,
176
+ * so the blur enlarges the splat's own (possibly anisotropic) footprint rather
177
+ * than adding a truly isotropic screen-space disc. The physically exact aperture
178
+ * model adds `coc²·I` to the *projected 2D* covariance with the same √(det)
179
+ * opacity conservation the antialias filter uses - that path is
180
+ * `SplatMesh.setDepthOfField` / `UnifiedSplatRenderer.setDepthOfField`
181
+ * (see `docs/guide/effects-and-modifiers.md`, M13).
182
+ *
183
+ * Opacity conservation is exact only with `antialias` on: without it, the
184
+ * fixed 0.3-px dilation dominates sub-pixel splats, so strong blur *dims*
185
+ * distant splats (their footprint barely grows while alpha still falls by
186
+ * 1/scale²) rather than blurring them.
187
+ */
188
+ export declare function depthOfFieldPreset(options?: {
189
+ focusDistance?: number;
190
+ aperture?: number;
191
+ maxBlur?: number;
192
+ }): DepthOfFieldPreset;
193
+ /** A preset that exposes a reveal `progress` uniform in `[0, 1]`. */
194
+ export interface RevealPreset {
195
+ readonly modifier: SplatModifier;
196
+ /** Reveal progress; 0 hides everything, 1 shows everything. */
197
+ readonly progress: {
198
+ value: number;
199
+ };
200
+ }
201
+ /**
202
+ * Time-driven dissolve/reveal. A value-noise field over the splat's local
203
+ * position is thresholded against `progress`: splats whose noise is below the
204
+ * threshold are shown, with a soft alpha edge. The noise is a `wgslFn` chunk -
205
+ * the M7 escape hatch - so this preset is **WebGPU-only** (see the fallback
206
+ * notes); force WebGL2 and it will not compile.
207
+ *
208
+ * The field is anchored to the mesh, so in a `SplatScene` a moved source
209
+ * reveals in step with whatever it now sits beside (and the pattern slides
210
+ * across it as it travels) rather than carrying its own schedule around.
211
+ */
212
+ export declare function revealPreset(options?: {
213
+ frequency?: number;
214
+ edge?: number;
215
+ }): RevealPreset;
216
+ /** A live world-warp preset: signed intensity and radius are mutable uniforms. */
217
+ export interface WorldWarpPreset {
218
+ readonly modifier: SplatModifier;
219
+ /**
220
+ * Signed warp amount in `[-1, 1]`. Positive wraps the street *down* so you
221
+ * walk on top of the sphere (tiny planet); negative wraps it *up* so you
222
+ * stand in the bowl (Inception fold). `0` is identity. Mutate `.value` to
223
+ * animate (no recompile).
224
+ */
225
+ readonly intensity: {
226
+ value: number;
227
+ };
228
+ /**
229
+ * Distance at which the wrap becomes strong. Nearby splats (`depth ≪
230
+ * radius`) stay put so walk/fly controls still read as moving through the
231
+ * unwarped near field. Must stay positive. Mutate `.value`.
232
+ */
233
+ readonly radius: {
234
+ value: number;
235
+ };
236
+ }
237
+ /**
238
+ * Camera-centered splat warp on the {@link SplatModifier} hook: one signed
239
+ * `intensity` uniform, two opposite hinges of the same view-space wrap. The
240
+ * camera is not moved - only splat centers (and their rigid orientation)
241
+ * change, so WASD / orbit stay as they are.
242
+ *
243
+ * Both sides hinge view-space `(y, z)` around camera +X by
244
+ * `(π/2) · atan(depth / radius)`:
245
+ *
246
+ * - `intensity > 0` - planet: the far street wraps *down* under your feet so
247
+ * you walk on top of the sphere.
248
+ * - `intensity < 0` - fold: the far street wraps *up* over your head so you
249
+ * stand in the bowl.
250
+ * - `intensity = 0` is identity (`offset = 0`, rotation unchanged).
251
+ *
252
+ * The shader mixes each full-strength hinge toward the original position by
253
+ * `|intensity|`. Orientation follows the orthonormalized Jacobian so
254
+ * ellipsoids wrap with the surface rather than sliding. Pure TSL - WebGPU
255
+ * and the WebGL2 fallback.
256
+ *
257
+ * Displaced splats keep their **pre-displacement** depth-sort order (the
258
+ * modifier-stack contract). Extreme warps can composite in the wrong order;
259
+ * this preset does not re-sort on warped centers.
260
+ */
261
+ export declare function worldWarpPreset(options?: {
262
+ intensity?: number;
263
+ radius?: number;
264
+ }): WorldWarpPreset;
265
+ /** Handle returned by {@link createRelightingProxy}. */
266
+ export interface RelightingProxy {
267
+ /** World-space group of lit proxy meshes (hide from the main splat scene). */
268
+ readonly group: THREE.Group;
269
+ /**
270
+ * Configures a material for the relight coverage pass: gray albedo and
271
+ * opaque alpha so uncovered RT pixels stay at clear-alpha 0.
272
+ */
273
+ configureMaterial(material: THREE.Material): void;
274
+ /** Disposes geometries and the default material owned by this helper. */
275
+ dispose(): void;
276
+ }
277
+ /**
278
+ * Node material for a **shadow-factor** lighting RT used with
279
+ * {@link SplatMesh.setRelighting}.
280
+ *
281
+ * Fragment output is `vec4(vec3(mix(umbra, 1, shadow(light))), 1)`:
282
+ * - covered + lit → RGB ≈ 1 (identity modulate when brightness/background are 1)
283
+ * - covered + umbra → RGB ≈ `umbra` (soft darken; never pure black)
284
+ * - uncovered stays clear-alpha 0 from the RT clear
285
+ *
286
+ * Clear the lighting RT to **RGB 1, A 0** (not black). Softness / bilinear
287
+ * samples at coverage edges otherwise pull in black and draw a dark outline
288
+ * of every collision triangle.
289
+ *
290
+ * Prefer this over a lit MeshStandard / ShadowMaterial pass when the host wants
291
+ * cast umbras without a static dark stamp on the whole collision footprint.
292
+ * Assign to proxy meshes with `castShadow` and `receiveShadow` both true; the
293
+ * umbra shape follows those triangles (splat foliage cannot cast).
294
+ *
295
+ * @param light - Shadow-casting light (typically a {@link THREE.DirectionalLight}).
296
+ * @param options.umbra - Multiplier in full shadow, in `[0, 1]`. Default `0.45`.
297
+ */
298
+ export declare function createRelightingShadowFactorMaterial(light: THREE.Light, options?: {
299
+ umbra?: number;
300
+ }): THREE.MeshStandardNodeMaterial;
301
+ /**
302
+ * Builds a PlayCanvas-style relighting **proxy** from collision tiles or raw
303
+ * geometries. The host places {@link RelightingProxy.group} on a dedicated
304
+ * layer / scene with lights (never the main splat scene), renders that into
305
+ * an RGBA RT matching the main camera, then calls
306
+ * {@link SplatMesh.setRelighting} with the RT texture.
307
+ *
308
+ * Collision meshes are a convenient stand-in when their silhouette is good
309
+ * enough; a denser reconstructed mesh is often better. This helper does not
310
+ * own lights or the render target.
311
+ *
312
+ * When `matrixWorld` is set (e.g. streamed mesh world matrix with the LCC
313
+ * Z-up→Y-up correction), it is **baked into the geometry** the same way the
314
+ * demo collision BVH path does - so the proxy aligns with splats without
315
+ * depending on a live Object3D transform.
316
+ *
317
+ * Coarse collision proxies leave hard coverage silhouettes; hosts should pass
318
+ * `RelightingSettings.softness` (and often a half-res lighting RT) so those
319
+ * edges do not look like a static shadow.
320
+ *
321
+ * For cast umbras **without** a static dark footprint, render the proxy with
322
+ * {@link createRelightingShadowFactorMaterial} so RT RGB is a multiplier
323
+ * (unshadowed ≈ 1), not a lit-gray appearance.
324
+ */
325
+ export declare function createRelightingProxy(options?: {
326
+ /** LCC collision tiles (source-local). Prefer with `matrixWorld`. */
327
+ tiles?: readonly CollisionMeshTile[];
328
+ /** Extra or alternative geometries already in the desired frame. */
329
+ geometries?: readonly THREE.BufferGeometry[];
330
+ /**
331
+ * Source-local → world (e.g. streamed mesh `matrixWorld`). Baked into
332
+ * tile / cloned geometries; leave unset when geometries are already world.
333
+ */
334
+ matrixWorld?: THREE.Matrix4;
335
+ /** Gray albedo in `[0, 1]`. Default `0.5` (PlayCanvas brightness 2). */
336
+ albedo?: number;
337
+ }): RelightingProxy;
@@ -0,0 +1,180 @@
1
+ import * as g from "three/webgpu";
2
+ import { shadow as te, mix as x, float as i, vec4 as O, vec3 as h, uniform as C, wgslFn as se, uniformArray as ne, smoothstep as X, int as oe, mat3 as re, modelViewMatrix as ae, vec2 as ce } from "three/tsl";
3
+ import { w as ie } from "./logging-BfPdd7NJ.js";
4
+ const Q = 5, le = { tint: 0, desaturate: 1, hide: 2, rim: 3 }, de = [0, 0, 0], ue = [1, 1, 1], fe = [0, 0, 0, 1];
5
+ function ye(o = [], n = {}) {
6
+ const r = Math.max(1, Math.floor(n.maxShapes ?? 32)), t = [];
7
+ for (let e = 0; e < r * Q; e++) t.push(new g.Vector4());
8
+ const d = ne(t, "vec4"), f = C(0, "int");
9
+ let u = !1;
10
+ const l = (e) => {
11
+ const m = Math.min(e.length, r);
12
+ e.length > r && !u && (u = !0, ie(
13
+ `sdfEffects: ${e.length} shapes exceeds maxShapes=${r}; extra shapes are ignored. Raise maxShapes if you need more.`
14
+ ));
15
+ for (let c = 0; c < m; c++) {
16
+ const s = e[c];
17
+ if (s.kind === "box") {
18
+ const a = s.halfExtents;
19
+ if (!a || !(a[0] > 0) || !(a[1] > 0) || !(a[2] > 0))
20
+ throw new Error(
21
+ `sdfEffects.setShapes: shape ${c} is a box and requires halfExtents with three positive components (got ${a ? `[${a.join(", ")}]` : "undefined"}).`
22
+ );
23
+ } else if (typeof s.radius == "number" && s.radius > 0) {
24
+ if (s.kind === "cylinder" && !(typeof s.height == "number" && s.height > 0))
25
+ throw new Error(
26
+ `sdfEffects.setShapes: shape ${c} is a cylinder and requires a positive height (got ${String(s.height)}).`
27
+ );
28
+ } else throw new Error(
29
+ `sdfEffects.setShapes: shape ${c} is a ${s.kind} and requires a positive radius (got ${String(s.radius)}).`
30
+ );
31
+ }
32
+ for (let c = 0; c < m; c++) {
33
+ const s = e[c], a = c * Q, w = s.center ?? de, p = s.color ?? ue, y = s.rotation ?? fe, E = s.kind === "box" ? 1 : s.kind === "cylinder" ? 2 : 0;
34
+ if (t[a + 0].set(w[0], w[1], w[2], E), s.kind === "box") {
35
+ const v = s.halfExtents;
36
+ t[a + 1].set(v[0], v[1], v[2], s.falloff ?? 0);
37
+ } else s.kind === "cylinder" ? t[a + 1].set(
38
+ s.radius,
39
+ s.height / 2,
40
+ 0,
41
+ s.falloff ?? 0
42
+ ) : t[a + 1].set(s.radius, 0, 0, s.falloff ?? 0);
43
+ t[a + 2].set(p[0], p[1], p[2], le[s.mode]), t[a + 3].set(y[0], y[1], y[2], y[3]), t[a + 4].set(s.invert ? 1 : 0, s.strength ?? 1, 0, 0);
44
+ }
45
+ f.value = m;
46
+ };
47
+ return l(o), { modifier: (e) => {
48
+ const m = e.localCenter;
49
+ let c = e.color.rgb, s = i(1);
50
+ for (let a = 0; a < r; a++) {
51
+ const w = a * Q, p = (ee) => d.element(w + ee), y = p(0), E = p(1), v = p(2), R = p(3), D = p(4), U = y.xyz, P = y.w, T = E.xyz, $ = E.w.max(1e-4), B = v.xyz, M = v.w, L = D.x, N = D.y, V = oe(a).lessThan(f).select(i(1), i(0)), I = m.sub(U), z = xe(R, I), A = z.length().sub(T.x), k = me(z, T), q = he(z, T.x, T.y), F = P.greaterThan(0.5).and(P.lessThan(1.5)).select(i(1), i(0)), G = P.greaterThan(1.5).select(i(1), i(0)), W = x(x(A, k, F), q, G), _ = x(W, W.negate(), L), S = i(1).sub(X(0, $, _)).mul(N).mul(V), j = i(1).sub(X(0, $, _.abs())).mul(N).mul(V), H = M.lessThan(0.5).select(i(1), i(0)), J = M.greaterThan(0.5).and(M.lessThan(1.5)).select(i(1), i(0)), K = M.greaterThan(1.5).and(M.lessThan(2.5)).select(i(1), i(0)), Y = M.greaterThan(2.5).select(i(1), i(0)), Z = c.dot(h(0.2126, 0.7152, 0.0722));
52
+ c = x(c, B, S.mul(H)), c = x(c, h(Z), S.mul(J)), c = x(c, B, j.mul(Y)), s = s.mul(i(1).sub(S.mul(K)));
53
+ }
54
+ return { color: O(c, e.color.a.mul(s)) };
55
+ }, setShapes: l, maxShapes: r, _uniforms: { slots: t, count: f } };
56
+ }
57
+ function me(o, n) {
58
+ const r = o.abs().sub(n), t = r.max(h(0, 0, 0)).length(), d = r.x.max(r.y.max(r.z)).min(0);
59
+ return t.add(d);
60
+ }
61
+ function he(o, n, r) {
62
+ const t = o.xz.length().sub(n), d = o.y.abs().sub(r), f = ce(t.max(0), d.max(0)).length(), u = t.max(d).min(0);
63
+ return f.add(u);
64
+ }
65
+ function xe(o, n) {
66
+ const r = o.xyz, t = o.w, d = r.cross(n).mul(2);
67
+ return n.sub(d.mul(t)).add(r.cross(d));
68
+ }
69
+ function we(o = {}) {
70
+ const n = new g.Vector3(...o.direction ?? [0.3, 1, 0.6]).normalize(), r = C(n), t = i(o.ambient ?? 0.35), d = i(o.diffuse ?? 0.75);
71
+ return { modifier: (u) => {
72
+ const l = u.normal.dot(r.normalize()).max(0), b = t.add(l.mul(d));
73
+ return { color: O(u.color.rgb.mul(b), u.color.a) };
74
+ }, direction: r };
75
+ }
76
+ function ve(o = {}) {
77
+ const n = C(Math.max(1e-4, o.focusDistance ?? 10)), r = C(Math.max(0, o.aperture ?? 0.5)), t = i(Math.max(0, o.maxBlur ?? 6));
78
+ return { modifier: (f) => {
79
+ const u = f.viewCenter.z.negate().max(1e-4), l = n.max(1e-4), b = u.sub(l).abs().div(l).mul(r).min(t), e = i(1).add(b), m = f.color.a.div(e.mul(e));
80
+ return { scale: f.scale.mul(e), color: O(f.color.rgb, m) };
81
+ }, focusDistance: n, aperture: r };
82
+ }
83
+ function Me(o = {}) {
84
+ const n = C(0), r = o.frequency ?? 3, t = Math.min(Math.max(o.edge ?? 0.08, 0), 0.5), d = se(
85
+ /* wgsl */
86
+ `
87
+ fn vnoise( x: vec3<f32> ) -> f32 {
88
+ let i = floor( x );
89
+ let g = fract( x );
90
+ let u = g * g * ( 3.0 - 2.0 * g );
91
+ let k = vec3<f32>( 12.9898, 78.233, 37.719 );
92
+ let s = 43758.5453;
93
+ let h000 = fract( sin( dot( i + vec3<f32>( 0.0, 0.0, 0.0 ), k ) ) * s );
94
+ let h100 = fract( sin( dot( i + vec3<f32>( 1.0, 0.0, 0.0 ), k ) ) * s );
95
+ let h010 = fract( sin( dot( i + vec3<f32>( 0.0, 1.0, 0.0 ), k ) ) * s );
96
+ let h110 = fract( sin( dot( i + vec3<f32>( 1.0, 1.0, 0.0 ), k ) ) * s );
97
+ let h001 = fract( sin( dot( i + vec3<f32>( 0.0, 0.0, 1.0 ), k ) ) * s );
98
+ let h101 = fract( sin( dot( i + vec3<f32>( 1.0, 0.0, 1.0 ), k ) ) * s );
99
+ let h011 = fract( sin( dot( i + vec3<f32>( 0.0, 1.0, 1.0 ), k ) ) * s );
100
+ let h111 = fract( sin( dot( i + vec3<f32>( 1.0, 1.0, 1.0 ), k ) ) * s );
101
+ let x00 = mix( h000, h100, u.x );
102
+ let x10 = mix( h010, h110, u.x );
103
+ let x01 = mix( h001, h101, u.x );
104
+ let x11 = mix( h011, h111, u.x );
105
+ let y0 = mix( x00, x10, u.y );
106
+ let y1 = mix( x01, x11, u.y );
107
+ return mix( y0, y1, u.z );
108
+ }
109
+ `
110
+ );
111
+ return { modifier: (u) => {
112
+ const l = d({ x: u.localCenter.mul(r) }), b = l.mul(1 - 2 * t).add(t), e = X(b.sub(t), b.add(t), n), m = n.greaterThan(b.sub(t));
113
+ return { color: O(u.color.rgb, u.color.a.mul(e)), visible: m };
114
+ }, progress: n };
115
+ }
116
+ function ke(o = {}) {
117
+ const n = C(Math.min(1, Math.max(-1, o.intensity ?? 0))), r = C(Math.max(1e-4, o.radius ?? 1));
118
+ return { modifier: (d) => {
119
+ const f = n.clamp(-1, 1), u = r.max(1e-4), l = d.viewCenter, b = f.max(0), e = f.negate().max(0), m = l.z.negate().max(0), c = m.div(u).atan().mul(i(Math.PI * 0.5)), s = c.cos(), a = c.sin(), w = a.negate(), p = h(l.x, l.y.mul(s).sub(l.z.mul(a)), l.y.mul(a).add(l.z.mul(s))), y = h(l.x, l.y.mul(s).sub(l.z.mul(w)), l.y.mul(w).add(l.z.mul(s))), E = x(x(l, p, e), y, b), v = i(Math.PI * 0.5).mul(u), R = l.z.lessThan(0).select(v.negate().div(m.mul(m).add(u.mul(u))), i(0)), D = R.negate(), U = h(1, 0, 0), P = h(0, s, a), T = h(0, a.negate().sub(p.z.mul(R)), s.add(p.y.mul(R))), $ = h(1, 0, 0), B = h(0, s, w), M = h(
120
+ 0,
121
+ w.negate().sub(y.z.mul(D)),
122
+ s.add(y.y.mul(D))
123
+ ), L = h(1, 0, 0), N = h(0, 1, 0), V = h(0, 0, 1), I = x(x(L, U, e), $, b), z = x(x(N, P, e), B, b), A = x(x(V, T, e), M, b), k = I.div(I.length().max(1e-6)), q = z.sub(k.mul(k.dot(z))), F = q.div(q.length().max(1e-6)), G = A.sub(k.mul(k.dot(A))).sub(F.mul(F.dot(A))), W = G.div(G.length().max(1e-6)), _ = re(k, F, W), S = ae.toMat3(), j = S.transpose(), H = j.mul(_).mul(S);
124
+ return {
125
+ offset: d.offset.add(j.mul(E.sub(l))),
126
+ rotation: H.mul(d.rotation)
127
+ };
128
+ }, intensity: n, radius: r };
129
+ }
130
+ function Ce(o, n = {}) {
131
+ const r = Number.isFinite(n.umbra) ? Math.min(1, Math.max(0, n.umbra)) : 0.45, t = new g.MeshStandardNodeMaterial();
132
+ t.side = g.DoubleSide, t.color = new g.Color(1, 1, 1), t.roughness = 1, t.metalness = 0, t.transparent = !1, t.depthWrite = !0, t.polygonOffset = !0, t.polygonOffsetFactor = 1, t.polygonOffsetUnits = 1;
133
+ const d = te(o), f = x(i(r), i(1), d);
134
+ return t.outputNode = O(h(f), i(1)), t;
135
+ }
136
+ function ge(o) {
137
+ const n = new g.BufferGeometry();
138
+ return n.setAttribute("position", new g.BufferAttribute(o.positions.slice(), 3)), n.setIndex(new g.BufferAttribute(o.indices.slice(), 1)), n;
139
+ }
140
+ function Ee(o = {}) {
141
+ const n = o.albedo ?? 0.5, r = new g.Group(), t = o.matrixWorld ?? null, d = [], f = new g.MeshStandardMaterial({
142
+ color: new g.Color(n, n, n),
143
+ roughness: 1,
144
+ metalness: 0,
145
+ side: g.DoubleSide
146
+ }), u = (e) => {
147
+ t && e.applyMatrix4(t);
148
+ }, l = (e, m) => {
149
+ let c = e, s = m;
150
+ t && !m && (c = e.clone(), s = !0), u(c), c.computeVertexNormals(), s && d.push(c);
151
+ const a = new g.Mesh(c, f);
152
+ a.castShadow = !0, a.receiveShadow = !0, a.raycast = () => {
153
+ }, r.add(a);
154
+ };
155
+ for (const e of o.tiles ?? [])
156
+ l(ge(e.data), !0);
157
+ for (const e of o.geometries ?? [])
158
+ l(e, !1);
159
+ return {
160
+ group: r,
161
+ configureMaterial: (e) => {
162
+ e.side = g.DoubleSide, e.transparent = !1, e.opacity = 1, "color" in e && e.color instanceof g.Color && e.color.setRGB(n, n, n), "roughness" in e && typeof e.roughness == "number" && (e.roughness = 1), "metalness" in e && typeof e.metalness == "number" && (e.metalness = 0);
163
+ },
164
+ dispose: () => {
165
+ f.dispose();
166
+ for (const e of d) e.dispose();
167
+ r.clear();
168
+ }
169
+ };
170
+ }
171
+ export {
172
+ Ee as createRelightingProxy,
173
+ Ce as createRelightingShadowFactorMaterial,
174
+ ve as depthOfFieldPreset,
175
+ we as lightingPreset,
176
+ Me as revealPreset,
177
+ ye as sdfEffects,
178
+ ke as worldWarpPreset
179
+ };
180
+ //# sourceMappingURL=effects.js.map