@voluma/vlam 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +60 -0
- package/THIRD_PARTY_NOTICES.md +86 -0
- package/dist/budget-governor.d.ts +154 -0
- package/dist/camera-budget-governor.d.ts +234 -0
- package/dist/chunk-cache-budget.d.ts +156 -0
- package/dist/chunk-fetch-scheduler.d.ts +168 -0
- package/dist/chunk-loader.d.ts +85 -0
- package/dist/compute-sorter.d.ts +131 -0
- package/dist/create-splat-renderer.d.ts +99 -0
- package/dist/dataset-source.d.ts +55 -0
- package/dist/depth-of-field.d.ts +73 -0
- package/dist/effects.d.ts +337 -0
- package/dist/effects.js +180 -0
- package/dist/effects.js.map +1 -0
- package/dist/formats/ksplat/index.d.ts +8 -0
- package/dist/formats/ksplat/parse-ksplat.d.ts +9 -0
- package/dist/formats/ksplat.js +167 -0
- package/dist/formats/ksplat.js.map +1 -0
- package/dist/formats/lcc/collision-mesh.d.ts +37 -0
- package/dist/formats/lcc/collision-partition.d.ts +35 -0
- package/dist/formats/lcc/index.d.ts +16 -0
- package/dist/formats/lcc/lcc.d.ts +28 -0
- package/dist/formats/lcc/lcc2-transform.d.ts +18 -0
- package/dist/formats/lcc/lcc2.d.ts +4 -0
- package/dist/formats/lcc/parse-collision-lci.d.ts +28 -0
- package/dist/formats/lcc/parse-lcc.d.ts +177 -0
- package/dist/formats/lcc/parse-mesh-ply.d.ts +21 -0
- package/dist/formats/lcc.js +969 -0
- package/dist/formats/lcc.js.map +1 -0
- package/dist/formats/ply/index.d.ts +12 -0
- package/dist/formats/ply/parse-compressed-ply.d.ts +16 -0
- package/dist/formats/ply/parse-splat-ply.d.ts +46 -0
- package/dist/formats/ply.js +230 -0
- package/dist/formats/ply.js.map +1 -0
- package/dist/formats/rad/frontier-worker-protocol.d.ts +177 -0
- package/dist/formats/rad/index.d.ts +10 -0
- package/dist/formats/rad/parse-rad.d.ts +88 -0
- package/dist/formats/rad/rad-column-decoders.d.ts +15 -0
- package/dist/formats/rad/rad-foveated-source.d.ts +71 -0
- package/dist/formats/rad/rad-parent-sizes.d.ts +45 -0
- package/dist/formats/rad/rad.d.ts +132 -0
- package/dist/formats/rad.js +1098 -0
- package/dist/formats/rad.js.map +1 -0
- package/dist/formats/sog/index.d.ts +9 -0
- package/dist/formats/sog/parse-sog.d.ts +45 -0
- package/dist/formats/sog.js +257 -0
- package/dist/formats/sog.js.map +1 -0
- package/dist/formats/splat/index.d.ts +8 -0
- package/dist/formats/splat/parse-splat.d.ts +10 -0
- package/dist/formats/splat.js +39 -0
- package/dist/formats/splat.js.map +1 -0
- package/dist/formats/spz/index.d.ts +8 -0
- package/dist/formats/spz/parse-spz.d.ts +10 -0
- package/dist/formats/spz.js +218 -0
- package/dist/formats/spz.js.map +1 -0
- package/dist/frontier-worker-CAnK_Cmu.js +566 -0
- package/dist/frontier-worker-CAnK_Cmu.js.map +1 -0
- package/dist/half-float-Cs4FDq8Q.js +30 -0
- package/dist/half-float-Cs4FDq8Q.js.map +1 -0
- package/dist/half-float.d.ts +15 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +9054 -0
- package/dist/index.js.map +1 -0
- package/dist/load-scene.d.ts +50 -0
- package/dist/load-worker-protocol.d.ts +82 -0
- package/dist/loading-wLo8vRbA.js +102 -0
- package/dist/loading-wLo8vRbA.js.map +1 -0
- package/dist/loading.d.ts +150 -0
- package/dist/lod-manifest.d.ts +61 -0
- package/dist/lod-scheduler.d.ts +229 -0
- package/dist/lod-source.d.ts +207 -0
- package/dist/logging-BfPdd7NJ.js +19 -0
- package/dist/logging-BfPdd7NJ.js.map +1 -0
- package/dist/logging.d.ts +37 -0
- package/dist/one-shot-worker-G80btnth.js +664 -0
- package/dist/one-shot-worker-G80btnth.js.map +1 -0
- package/dist/orientation.d.ts +35 -0
- package/dist/ply-header-BwgExCn-.js +125 -0
- package/dist/ply-header-BwgExCn-.js.map +1 -0
- package/dist/ply-header.d.ts +87 -0
- package/dist/radix-sort.d.ts +28 -0
- package/dist/radix-sorter.d.ts +46 -0
- package/dist/relighting.d.ts +50 -0
- package/dist/selection-volume.d.ts +87 -0
- package/dist/sh-pack-D5wAe5gg.js +54 -0
- package/dist/sh-pack-D5wAe5gg.js.map +1 -0
- package/dist/sh-pack.d.ts +49 -0
- package/dist/sort-scheduler.d.ts +62 -0
- package/dist/sort-worker-protocol.d.ts +37 -0
- package/dist/sorter.d.ts +31 -0
- package/dist/source-transform.d.ts +57 -0
- package/dist/splat-budget-PSojLJPO.js +157 -0
- package/dist/splat-budget-PSojLJPO.js.map +1 -0
- package/dist/splat-budget.d.ts +404 -0
- package/dist/splat-data-BZM5dhhx.js +10 -0
- package/dist/splat-data-BZM5dhhx.js.map +1 -0
- package/dist/splat-data.d.ts +150 -0
- package/dist/splat-depth-pack.d.ts +39 -0
- package/dist/splat-mesh-material.d.ts +255 -0
- package/dist/splat-mesh-picking.d.ts +87 -0
- package/dist/splat-mesh-pool.d.ts +241 -0
- package/dist/splat-mesh-types-8QDjTbZO.js +390 -0
- package/dist/splat-mesh-types-8QDjTbZO.js.map +1 -0
- package/dist/splat-mesh-types.d.ts +390 -0
- package/dist/splat-mesh.d.ts +737 -0
- package/dist/splat-modifier-stack.d.ts +38 -0
- package/dist/splat-modifier.d.ts +156 -0
- package/dist/splat-partition.d.ts +37 -0
- package/dist/splat-query.d.ts +51 -0
- package/dist/splat-scene.d.ts +127 -0
- package/dist/splat-sort-bounds.d.ts +11 -0
- package/dist/static-lod-splat-mesh.d.ts +53 -0
- package/dist/static-lod-worker-D2fyqQZO.js +474 -0
- package/dist/static-lod-worker-D2fyqQZO.js.map +1 -0
- package/dist/static-lod-worker-protocol.d.ts +37 -0
- package/dist/static-lod.d.ts +24 -0
- package/dist/storage-attribute-mirror.d.ts +103 -0
- package/dist/streamed-splat-mesh-utils.d.ts +146 -0
- package/dist/streamed-splat-mesh.d.ts +985 -0
- package/dist/unified-splat-renderer.d.ts +205 -0
- package/dist/webgpu-limits.d.ts +133 -0
- package/dist/work-buffer-gather.d.ts +119 -0
- package/dist/work-buffer-material.d.ts +42 -0
- package/dist/worker-sorter.d.ts +57 -0
- package/dist/xr-view.d.ts +92 -0
- package/package.json +135 -21
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device profiling and the default active-splat budget.
|
|
3
|
+
*
|
|
4
|
+
* Streaming is splat-count budgeted rather than byte budgeted: every cost in
|
|
5
|
+
* the renderer (pool textures, sort buffers) scales with the number of
|
|
6
|
+
* resident splats - roughly 64 bytes of GPU memory per splat (three RGBA32F
|
|
7
|
+
* data textures + one RGBA8, plus the sort index/order/bucket buffers), or
|
|
8
|
+
* about 48 bytes when `poolFloatTextures: 'float16'` halves centers and
|
|
9
|
+
* covarianceA; plus about 52 more bytes of CPU-side backing per splat of
|
|
10
|
+
* pool capacity. Capping the active splat count therefore caps memory
|
|
11
|
+
* directly, which is what matters on mobile GPUs.
|
|
12
|
+
*/
|
|
13
|
+
import type { StreamedSplatFormat } from './loading';
|
|
14
|
+
/**
|
|
15
|
+
* Desktop GPU capability class for budget / quality defaults.
|
|
16
|
+
*
|
|
17
|
+
* Mobile still keys off {@link SplatDeviceProfile.isMobile}; this splits
|
|
18
|
+
* non-mobile machines that would otherwise share the ~8M workstation path.
|
|
19
|
+
* Unset means "unknown" - keep the pre-existing desktop defaults (fail open).
|
|
20
|
+
*/
|
|
21
|
+
export type SplatGpuClass = 'discrete' | 'integrated' | 'fallback';
|
|
22
|
+
/** Browser/device signals used for deterministic budget selection. */
|
|
23
|
+
export interface SplatDeviceProfile {
|
|
24
|
+
/** Coarse memory estimate in GiB, when exposed by the browser. */
|
|
25
|
+
deviceMemoryGb?: number;
|
|
26
|
+
/** Whether the runtime is an iOS/iPadOS-class device. */
|
|
27
|
+
isIOS?: boolean;
|
|
28
|
+
/** Whether the runtime is a phone/tablet-class device (includes iOS). */
|
|
29
|
+
isMobile?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the runtime is a standalone XR headset browser (Quest, Pico, …).
|
|
32
|
+
* Headsets are mobile-class GPUs asked to fill a stereo framebuffer larger
|
|
33
|
+
* than a 4K desktop, so they get their own, tighter defaults.
|
|
34
|
+
*/
|
|
35
|
+
isHeadset?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether this is a *budget* phone rather than a flagship one.
|
|
38
|
+
*
|
|
39
|
+
* `isMobile` cannot tell a Galaxy A51 from an iPhone 15 Pro, and the memory
|
|
40
|
+
* signal cannot either - Chrome privacy-caps `deviceMemory` at 8 GiB, so the
|
|
41
|
+
* flagship and the mid-ranger both land on the same ceiling despite an order
|
|
42
|
+
* of magnitude between their GPUs. A coarse signal, and a *downward* one: a
|
|
43
|
+
* reading of ≤4 is trustworthy because the cap only ever lowers it, while a
|
|
44
|
+
* reading of 8 says nothing. {@link hasWebGpu} is the sharper signal where it
|
|
45
|
+
* applies, since it names a difference in how the sort runs rather than
|
|
46
|
+
* guessing capability from RAM.
|
|
47
|
+
*/
|
|
48
|
+
isLowPower?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the runtime exposes WebGPU at all.
|
|
51
|
+
*
|
|
52
|
+
* `false` means the renderer will take the WebGL2 fallback, where the depth
|
|
53
|
+
* sort runs on the CPU rather than as a GPU compute pass - a different cost
|
|
54
|
+
* curve, not just a slower one. See {@link resolveSplatBudget}.
|
|
55
|
+
*
|
|
56
|
+
* Detected from the presence of `navigator.gpu`, which is a *browser support*
|
|
57
|
+
* signal rather than an adapter one: a runtime that exposes the API but whose
|
|
58
|
+
* `requestAdapter` fails (driver blocklist) still reports `true` here and will
|
|
59
|
+
* fall back to WebGL2 anyway. A host that knows its renderer's real backend
|
|
60
|
+
* can correct this by passing its own profile.
|
|
61
|
+
*/
|
|
62
|
+
hasWebGpu?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Desktop GPU class from {@link probeSplatGpuClass} / {@link classifySplatGpuClass}.
|
|
65
|
+
* Omit when unknown so workstations without adapter info keep the 8M path.
|
|
66
|
+
*/
|
|
67
|
+
gpuClass?: SplatGpuClass;
|
|
68
|
+
}
|
|
69
|
+
/** Adapter identifying fields used by {@link classifySplatGpuClass}. */
|
|
70
|
+
export interface SplatGpuAdapterInfo {
|
|
71
|
+
vendor?: string;
|
|
72
|
+
architecture?: string;
|
|
73
|
+
device?: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
isFallbackAdapter?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* How expensive a format's splats are *per splat*, which is not the same across
|
|
79
|
+
* formats and is the reason one mobile budget cannot serve them all.
|
|
80
|
+
*
|
|
81
|
+
* - `'sampled'` - coarser LOD levels are subsamples of the same surface, so a
|
|
82
|
+
* splat costs about the same however deep the cut is. Streamed SOG.
|
|
83
|
+
* - `'lcc'` - XGRIDS' levels are decimated *alternatives* whose splats merge
|
|
84
|
+
* into wide flat discs (see {@link liftBudgetToFinestLevel}). Cost per splat
|
|
85
|
+
* therefore *rises* as the budget falls, because a tighter budget serves
|
|
86
|
+
* coarser levels and each of their splats covers more pixels. `.rad`, `.lcc`,
|
|
87
|
+
* `.lcc2`.
|
|
88
|
+
*
|
|
89
|
+
* Measured on one iPhone 15 Pro, which is what the numbers below are worth:
|
|
90
|
+
* `oldtimers-route` (`.rad`) ran 31-39 fps at 750k and 45-50 at 600k, while
|
|
91
|
+
* `sandwijck` (streamed SOG) held 52-57 fps at 675k resident. Two streamed
|
|
92
|
+
* scenes, near-identical splat counts, ~20 fps apart - splat count is a poor
|
|
93
|
+
* proxy for cost, and the format is the only better signal available for free
|
|
94
|
+
* before a single byte is fetched.
|
|
95
|
+
*/
|
|
96
|
+
export type SplatCostClass = 'lcc' | 'sampled';
|
|
97
|
+
/**
|
|
98
|
+
* Classifies a WebGPU adapter from its identifying strings.
|
|
99
|
+
*
|
|
100
|
+
* Heuristics only - browsers may blank fields for privacy. Prefer
|
|
101
|
+
* {@link probeSplatGpuClass} when both power-preference adapters are available.
|
|
102
|
+
*/
|
|
103
|
+
export declare function classifySplatGpuClass(info: SplatGpuAdapterInfo | undefined): SplatGpuClass;
|
|
104
|
+
/** Minimal `navigator.gpu` surface for {@link probeSplatGpuClass}. */
|
|
105
|
+
export interface SplatGpuProbeEntry {
|
|
106
|
+
requestAdapter(options?: {
|
|
107
|
+
powerPreference?: 'low-power' | 'high-performance';
|
|
108
|
+
}): Promise<SplatGpuProbeAdapter | null>;
|
|
109
|
+
}
|
|
110
|
+
/** Adapter handle with optional `.info` (WebGPU GPUAdapterInfo). */
|
|
111
|
+
export interface SplatGpuProbeAdapter {
|
|
112
|
+
readonly info?: SplatGpuAdapterInfo;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Probes WebGPU adapters to choose a {@link SplatGpuClass}.
|
|
116
|
+
*
|
|
117
|
+
* Requests both `low-power` and `high-performance` adapters when possible. A
|
|
118
|
+
* hybrid laptop that exposes two different devices is `discrete`; a single
|
|
119
|
+
* shared / unified GPU (Apple Silicon, many iGPU-only machines) is classified
|
|
120
|
+
* from {@link classifySplatGpuClass}. Returns `undefined` when WebGPU is absent
|
|
121
|
+
* or the probe fails, so budget resolution keeps the legacy desktop path.
|
|
122
|
+
*/
|
|
123
|
+
export declare function probeSplatGpuClass(gpu?: SplatGpuProbeEntry | null | undefined): Promise<SplatGpuClass | undefined>;
|
|
124
|
+
/**
|
|
125
|
+
* Whether fill-rate / laptop-class defaults apply (mobile, or desktop
|
|
126
|
+
* integrated / software fallback). Exported so hosts and the demo share one
|
|
127
|
+
* predicate for performance-mode defaults and adaptive DPR.
|
|
128
|
+
*/
|
|
129
|
+
export declare function isFillConstrainedSplatDevice(profile?: SplatDeviceProfile | undefined): boolean;
|
|
130
|
+
/** Reads the available browser device signals without requiring a DOM runtime. */
|
|
131
|
+
export declare function detectSplatDeviceProfile(): SplatDeviceProfile | undefined;
|
|
132
|
+
/** Scene-side signals for {@link resolveSplatBudget}. */
|
|
133
|
+
export interface SplatBudgetOptions {
|
|
134
|
+
/**
|
|
135
|
+
* The format about to be loaded, which selects the {@link SplatCostClass}
|
|
136
|
+
* whose mobile tier applies. Omit when it is not yet known - the `'sampled'`
|
|
137
|
+
* numbers are what every caller resolved before cost classes existed.
|
|
138
|
+
*/
|
|
139
|
+
format?: StreamedSplatFormat;
|
|
140
|
+
/**
|
|
141
|
+
* A ceiling on the resolved default, for callers that want to tighten without
|
|
142
|
+
* overriding what this function knows.
|
|
143
|
+
*
|
|
144
|
+
* `override` is absolute: it wins over the device tier, the cost class and
|
|
145
|
+
* everything else, because a caller who names a number has said they know
|
|
146
|
+
* better. That is the right contract and the wrong tool for "the same as
|
|
147
|
+
* usual, but no more than N" - which is what a performance toggle or a host
|
|
148
|
+
* default actually means. Pinning a number there has twice shipped as a bug:
|
|
149
|
+
* a demo performance mode that *raised* the load on the weakest device tested,
|
|
150
|
+
* and a host default that bypassed every device tier.
|
|
151
|
+
*
|
|
152
|
+
* Applied only when `override` is omitted, and only downward.
|
|
153
|
+
*/
|
|
154
|
+
cap?: number;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Chooses a default active-splat budget for the current device.
|
|
158
|
+
*
|
|
159
|
+
* The default is derived from the coarsest device-memory signal the platform
|
|
160
|
+
* exposes; callers who know better (or want a hard cap for a mobile test on
|
|
161
|
+
* desktop) should pass an explicit override.
|
|
162
|
+
*
|
|
163
|
+
* @param override - Explicit budget in splats; wins over device detection when
|
|
164
|
+
* positive. Throws `RangeError` if it is not a positive finite number.
|
|
165
|
+
* @param profile - Device signals to decide from; defaults to detecting them.
|
|
166
|
+
* @param options - Scene signals. `format` selects the {@link SplatCostClass}
|
|
167
|
+
* whose mobile tier applies; omitting it keeps the `'sampled'` numbers, which
|
|
168
|
+
* are what every caller resolved before cost classes existed. `cap` is a
|
|
169
|
+
* ceiling on the *resolved default*, for callers who want to tighten without
|
|
170
|
+
* overriding - see {@link SplatBudgetOptions.cap}.
|
|
171
|
+
* @returns A splat-count budget, clamped to a sensible range.
|
|
172
|
+
*/
|
|
173
|
+
export declare function resolveSplatBudget(override?: number, profile?: SplatDeviceProfile | undefined, options?: SplatBudgetOptions): number;
|
|
174
|
+
/**
|
|
175
|
+
* The budget to run at **while an immersive session presents**, given whatever
|
|
176
|
+
* budget the page is using outside one.
|
|
177
|
+
*
|
|
178
|
+
* Stereo is the cost, and stereo is a property of the *session*, not of the
|
|
179
|
+
* device: every splat is drawn twice, into two eye viewports that together
|
|
180
|
+
* exceed a 4K desktop, on a GPU that must hold 72–90 Hz or the compositor
|
|
181
|
+
* reprojects. That is true of a standalone headset and equally true of a
|
|
182
|
+
* desktop machine driving a tethered one - which is exactly the case device
|
|
183
|
+
* sniffing cannot see, since such a machine is a desktop right up until the
|
|
184
|
+
* moment it is not.
|
|
185
|
+
*
|
|
186
|
+
* Keying off presentation instead of identity is also the only approach that
|
|
187
|
+
* survives new hardware. `detectSplatDeviceProfile`'s `isHeadset` is a
|
|
188
|
+
* user-agent guess: it misses Apple Vision Pro outright (visionOS Safari
|
|
189
|
+
* presents as desktop Safari, so a headset would otherwise take the multi-
|
|
190
|
+
* million desktop budget) and misses every headset released after it is
|
|
191
|
+
* written. This function needs to know none of that.
|
|
192
|
+
*
|
|
193
|
+
* Apply it on `sessionstart` and restore the original on `sessionend` - via
|
|
194
|
+
* `BudgetGovernor.setBudget` when several meshes share a pool, or a streamed
|
|
195
|
+
* mesh's own budget setter otherwise.
|
|
196
|
+
*
|
|
197
|
+
* @param pageBudget - The budget in use outside the session, typically from
|
|
198
|
+
* {@link resolveSplatBudget}. Throws `RangeError` if not a positive finite
|
|
199
|
+
* number.
|
|
200
|
+
* @returns `pageBudget` lowered to the stereo ceiling; never raised - a device
|
|
201
|
+
* already rendering below the ceiling stays where it is.
|
|
202
|
+
*/
|
|
203
|
+
export declare function resolveXrSplatBudget(pageBudget: number): number;
|
|
204
|
+
/**
|
|
205
|
+
* Raises `budget` far enough to hold a scene's finest level in full, when
|
|
206
|
+
* that level is small enough to be worth taking whole.
|
|
207
|
+
*
|
|
208
|
+
* The LOD budget assumes coarser levels are cheap *approximations* of the
|
|
209
|
+
* same surface, so trading detail for memory is nearly free. XGRIDS' LCC
|
|
210
|
+
* LCC breaks that assumption: its five levels are decimated alternatives
|
|
211
|
+
* whose splats merge into wide flat discs (correct at distance, streaked up
|
|
212
|
+
* close), so any budget shortfall costs visible quality - sub-chunking gives
|
|
213
|
+
* the scheduler ~128k-splat granularity to thin cells with, but a thinned
|
|
214
|
+
* fine level is still sparser than the capture intends. A capture whose
|
|
215
|
+
* finest level fits under the desktop ceiling is better shown whole: Casino's
|
|
216
|
+
* level 0 is 4.74M splats and sits under the 8M default.
|
|
217
|
+
*
|
|
218
|
+
* Mobile and fill-constrained desktops (integrated / fallback GPU class) are
|
|
219
|
+
* exempt: their cap is a fill-rate limit, not a sizing accident.
|
|
220
|
+
*
|
|
221
|
+
* @param budget - The resolved device budget.
|
|
222
|
+
* @param finestLevelSplats - Splats in the scene's finest level.
|
|
223
|
+
* @returns `budget`, or the finest level's size when that is the better fit.
|
|
224
|
+
*/
|
|
225
|
+
export declare function liftBudgetToFinestLevel(budget: number, finestLevelSplats: number, profile?: SplatDeviceProfile | undefined): number;
|
|
226
|
+
/** Options for {@link estimateSplatPoolBytes}. */
|
|
227
|
+
export interface SplatPoolBytesOptions {
|
|
228
|
+
/**
|
|
229
|
+
* Pool texture precision, matching `SplatMeshOptions.poolFloatTextures`.
|
|
230
|
+
* `'float16'` halves centers and covarianceA. Default `'float32'`.
|
|
231
|
+
*/
|
|
232
|
+
floatTextures?: 'float32' | 'float16';
|
|
233
|
+
/** Per-splat SH bands the pool allocates for. Default `0`. */
|
|
234
|
+
shBands?: 0 | 1 | 2 | 3;
|
|
235
|
+
/**
|
|
236
|
+
* Pool capacity as a multiple of `splats`. `StreamedSplatMesh` allocates
|
|
237
|
+
* 1.5× (the default here) so per-run row alignment and the
|
|
238
|
+
* append-before-remove window during LOD swaps have somewhere to go; pass
|
|
239
|
+
* `1.4` to model `experimentalStagedSwaps: false`, or `1` for a static mesh.
|
|
240
|
+
*/
|
|
241
|
+
capacityFactor?: number;
|
|
242
|
+
/**
|
|
243
|
+
* Whether to include the CPU-side backing arrays. Default `true` - they are
|
|
244
|
+
* real host memory a device has to find, so the honest answer to "can I
|
|
245
|
+
* afford this ceiling" includes them.
|
|
246
|
+
*/
|
|
247
|
+
includeCpuBacking?: boolean;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Estimates the memory a splat pool of `splats` costs.
|
|
251
|
+
*
|
|
252
|
+
* This exists to make a streamed mesh's `maxBudget` a computation rather than a
|
|
253
|
+
* guess. A pool is allocated **once, from the ceiling** and never
|
|
254
|
+
* grows, so several governed meshes cost the sum of their ceilings whatever the
|
|
255
|
+
* shared budget is set to - a `BudgetGovernor` redistributes *sharpness* within
|
|
256
|
+
* that envelope, it does not shrink it. Price the ceilings before choosing them:
|
|
257
|
+
*
|
|
258
|
+
* ```js
|
|
259
|
+
* // 1 main + 4 markers, each able to reach 1.5M splats
|
|
260
|
+
* const bytes = estimateSplatPoolBytes(4_000_000) + 4 * estimateSplatPoolBytes(1_500_000);
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* Per splat of *capacity* (`splats × capacityFactor`), counted from what the
|
|
264
|
+
* constructor actually allocates:
|
|
265
|
+
*
|
|
266
|
+
* - **Pool textures** 52 B - centers RGBA32F 16, colors RGBA8 4,
|
|
267
|
+
* covarianceA RGBA32F 16, covarianceB RGBA32F 16. `'float16'` drops centers
|
|
268
|
+
* and covarianceA to 8 each (36 B); covarianceB stays float32 because it
|
|
269
|
+
* packs integer IDs.
|
|
270
|
+
* - **Packed SH** 16 B per `RGBA32UI` texture, `ceil(coefficients / 4)` of them
|
|
271
|
+
* - so 16 / 32 / 64 B at 1 / 2 / 3 bands.
|
|
272
|
+
* - **Sort storage** 16 B - the radix sorter's ping-pong key/value buffers.
|
|
273
|
+
* - **CPU backing** 68 B - the float32/uint8 arrays kept for partial uploads
|
|
274
|
+
* (always full precision, even under `'float16'`): centers 16, colors 4,
|
|
275
|
+
* covarianceA 16, covarianceB 16, plus four u32-per-splat arrays - draw-order
|
|
276
|
+
* indices, the active-list source index, the pool-slot map, and the picker's
|
|
277
|
+
* pool-index template (allocated lazily, counted because a picked scene pays
|
|
278
|
+
* it). Under `'float16'` add a further 16 B: the half-encoded texture images
|
|
279
|
+
* are held alongside the float32 backing, not instead of it, so that
|
|
280
|
+
* precision saves GPU bytes only. Included unless `includeCpuBacking` is
|
|
281
|
+
* `false`.
|
|
282
|
+
*
|
|
283
|
+
* The module header's "roughly 64 bytes per splat" rounds the GPU side of this.
|
|
284
|
+
*
|
|
285
|
+
* @param splats - The ceiling in splats (e.g. a mesh's `maxBudget`).
|
|
286
|
+
* @returns Estimated bytes. Indicative, not a device-memory guarantee: driver
|
|
287
|
+
* texture padding, staging allocations and the decoded-chunk CPU cache
|
|
288
|
+
* ({@link resolveCpuCacheBytes}) sit outside it.
|
|
289
|
+
* @throws {RangeError} if `splats` is not a positive finite number, or
|
|
290
|
+
* `capacityFactor` is not a finite number `>= 1`.
|
|
291
|
+
*/
|
|
292
|
+
export declare function estimateSplatPoolBytes(splats: number, options?: SplatPoolBytesOptions): number;
|
|
293
|
+
/**
|
|
294
|
+
* Suggested ceiling for the renderer's pixel ratio on this device.
|
|
295
|
+
*
|
|
296
|
+
* Splat rendering is fragment-bound, so render resolution is one of the
|
|
297
|
+
* largest costs on a high-DPI phone: a 2.6x display renders ~7x the pixels of
|
|
298
|
+
* a 1x one, and every one of them blends the full depth-sorted splat stack.
|
|
299
|
+
* The library cannot apply this itself (it draws into a renderer the app
|
|
300
|
+
* owns), so it exports the policy - pass it to `renderer.setPixelRatio`:
|
|
301
|
+
*
|
|
302
|
+
* ```js
|
|
303
|
+
* renderer.setPixelRatio(Math.min(window.devicePixelRatio, recommendedMaxPixelRatio()));
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* This is the *quality* ceiling. Viewers that offer a performance mode
|
|
307
|
+
* (SuperSplat halves mobile resolution in its own) should go lower still when
|
|
308
|
+
* it is on.
|
|
309
|
+
*/
|
|
310
|
+
export declare function recommendedMaxPixelRatio(profile?: SplatDeviceProfile | undefined): number;
|
|
311
|
+
/**
|
|
312
|
+
* The Gaussian cutoff a `.rad` renders at, in standard deviations, or
|
|
313
|
+
* `undefined` to accept the device-wide default.
|
|
314
|
+
*
|
|
315
|
+
* Spark hard-codes `sqrt(8)` (≈2.83σ) for every device, and matching it is what
|
|
316
|
+
* makes a `.rad` look like Spark's render on a desktop. On a phone it is the
|
|
317
|
+
* wrong trade: rendering is fill-bound on mobile, so a format override bypasses
|
|
318
|
+
* the device policy that every other format accepts.
|
|
319
|
+
*
|
|
320
|
+
* Returning `undefined` on mobile lets `SplatMesh` apply its own 3σ cutoff and
|
|
321
|
+
* undersized-splat floor; `.rad` must not escape that mobile policy.
|
|
322
|
+
*
|
|
323
|
+
* @returns √8 on discrete desktop; `undefined` on fill-constrained devices,
|
|
324
|
+
* meaning "no format override".
|
|
325
|
+
*/
|
|
326
|
+
export declare function recommendedRadMaxStdDev(profile?: SplatDeviceProfile | undefined): number | undefined;
|
|
327
|
+
/**
|
|
328
|
+
* Suggested WebXR framebuffer scale for this device. On three 0.185.x this is
|
|
329
|
+
* a **WebGL XR** policy: apply it once before the session starts with
|
|
330
|
+
* `renderer.xr.setFramebufferScaleFactor`. three's WebGPU XR path creates its
|
|
331
|
+
* `XRGPUBinding` projection layer at native scale and does not consume that
|
|
332
|
+
* setting, so WebGPU hosts should leave the native scale and use the presenting
|
|
333
|
+
* splat budget plus fixed foveation for runtime headroom instead.
|
|
334
|
+
*
|
|
335
|
+
* Splat rendering is fill-bound, and a headset's default framebuffer is
|
|
336
|
+
* already supersampled past its panels (~1680×1760 per eye on Quest 3):
|
|
337
|
+
* 0.8 cuts fragment work ~36% for a barely visible softening. Non-headset
|
|
338
|
+
* XR (desktop-tethered) keeps the native 1.0.
|
|
339
|
+
*/
|
|
340
|
+
export declare function recommendedXrFramebufferScale(profile?: SplatDeviceProfile | undefined): number;
|
|
341
|
+
/** Inputs for {@link suggestAdaptivePixelRatio}. */
|
|
342
|
+
export interface AdaptivePixelRatioInput {
|
|
343
|
+
/** Latest wall-frame time in milliseconds. */
|
|
344
|
+
frameMs: number;
|
|
345
|
+
/** Currently applied pixel ratio. */
|
|
346
|
+
current: number;
|
|
347
|
+
/** Quality ceiling (typically {@link recommendedMaxPixelRatio}). */
|
|
348
|
+
max: number;
|
|
349
|
+
/** Floor; defaults to `1`. */
|
|
350
|
+
min?: number;
|
|
351
|
+
/** EMA of frame time from the previous call; omit on the first sample. */
|
|
352
|
+
emaMs?: number;
|
|
353
|
+
/**
|
|
354
|
+
* Comfortable frame time (ms). Below this (with headroom) the helper may
|
|
355
|
+
* step the ratio up. Default `18` (~55 fps).
|
|
356
|
+
*/
|
|
357
|
+
targetFrameMs?: number;
|
|
358
|
+
/**
|
|
359
|
+
* Sustained frame time (ms) that triggers a step down. Default `22`
|
|
360
|
+
* (~45 fps) so brief spikes do not thrash the canvas size.
|
|
361
|
+
*/
|
|
362
|
+
pressureFrameMs?: number;
|
|
363
|
+
}
|
|
364
|
+
/** Result of {@link suggestAdaptivePixelRatio}. */
|
|
365
|
+
export interface AdaptivePixelRatioResult {
|
|
366
|
+
/** Suggested pixel ratio after hysteresis (quarter steps). */
|
|
367
|
+
pixelRatio: number;
|
|
368
|
+
/** Updated EMA to pass back on the next call. */
|
|
369
|
+
emaMs: number;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Suggests a pixel ratio under frame-time pressure.
|
|
373
|
+
*
|
|
374
|
+
* The library cannot call `renderer.setPixelRatio` (the host owns the
|
|
375
|
+
* renderer); this is a pure policy helper. Pass the previous result's
|
|
376
|
+
* `emaMs` each frame for a stable EMA, and only re-size the canvas when
|
|
377
|
+
* `pixelRatio` changes.
|
|
378
|
+
*
|
|
379
|
+
* Steps are quarter-units with asymmetric thresholds (pressure to lower,
|
|
380
|
+
* comfortable headroom to raise) so the ratio does not oscillate.
|
|
381
|
+
*/
|
|
382
|
+
export declare function suggestAdaptivePixelRatio(input: AdaptivePixelRatioInput): AdaptivePixelRatioResult;
|
|
383
|
+
/**
|
|
384
|
+
* Chooses a decoded-chunk CPU cache cap from the same safe device profile.
|
|
385
|
+
*
|
|
386
|
+
* **The unknown-memory fallback must not read as "tiny".** iOS Safari does not
|
|
387
|
+
* implement `navigator.deviceMemory` at all, so every iPhone lands on this
|
|
388
|
+
* branch - and an earlier version resolved it to 1 GiB, i.e. the 32 MiB floor,
|
|
389
|
+
* on hardware with 8 GiB. That is not a conservative guess, it is a wrong one,
|
|
390
|
+
* and it is invisible on `.rad`, whose page-table cache has its own floor.
|
|
391
|
+
*
|
|
392
|
+
* On streamed SOG there is no such floor and the cost is immediate: measured on
|
|
393
|
+
* an iPhone 15 Pro against `sandwijck`, the scheduler asked for 539,734 splats
|
|
394
|
+
* and the mesh could only hold 466,499 of them across 3 chunk files (desktop:
|
|
395
|
+
* 5), evicting continuously. The finest level is wanted *nearest the camera* and
|
|
396
|
+
* lives in the largest files, so those are what fail to stay resident - the
|
|
397
|
+
* middle of the view drops out and the scene renders as a donut.
|
|
398
|
+
*
|
|
399
|
+
* A device that declines to report its memory is far likelier to be a modern
|
|
400
|
+
* phone withholding a fingerprinting signal than an actual 1 GiB device, so the
|
|
401
|
+
* fallback assumes 4 GiB (128 MiB of cache). The floor stays for the genuinely
|
|
402
|
+
* profile-less case - SSR and Node tooling, which never stream a scene anyway.
|
|
403
|
+
*/
|
|
404
|
+
export declare function resolveCpuCacheBytes(profile?: SplatDeviceProfile | undefined): number;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const z = 0.28209479177387814;
|
|
2
|
+
function A(c, s, H, L, M, _, b, j, k) {
|
|
3
|
+
const h = Math.hypot(_, b, j, k) || 1, m = _ / h, t = b / h, n = j / h, o = k / h, r = (1 - 2 * (n * n + o * o)) * H, p = 2 * (t * n - m * o) * L, w = 2 * (t * o + m * n) * M, C = 2 * (t * n + m * o) * H, S = (1 - 2 * (t * t + o * o)) * L, f = 2 * (n * o - m * t) * M, g = 2 * (t * o - m * n) * H, v = 2 * (n * o + m * t) * L, y = (1 - 2 * (t * t + n * n)) * M;
|
|
4
|
+
c[s * 6 + 0] = r * r + p * p + w * w, c[s * 6 + 1] = r * C + p * S + w * f, c[s * 6 + 2] = r * g + p * v + w * y, c[s * 6 + 3] = C * C + S * S + f * f, c[s * 6 + 4] = C * g + S * v + f * y, c[s * 6 + 5] = g * g + v * v + y * y;
|
|
5
|
+
}
|
|
6
|
+
export {
|
|
7
|
+
z as S,
|
|
8
|
+
A as w
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=splat-data-BZM5dhhx.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splat-data-BZM5dhhx.js","sources":["../src/lib/splat-data.ts"],"sourcesContent":["import type { SplatSourceFormat } from './loading';\n\n/**\n * CPU-side representation of a set of 3D Gaussians, ready for GPU upload.\n *\n * Everything is stored in flat typed arrays (struct-of-arrays) so the data\n * can be copied straight into textures and transferred to workers.\n */\nexport interface SplatData {\n /** Number of Gaussians. */\n readonly count: number;\n /** Gaussian centers, 3 floats (x, y, z) per splat. */\n readonly positions: Float32Array;\n /** Base color + opacity, 4 bytes (r, g, b, a) per splat. */\n readonly colors: Uint8Array;\n /**\n * 3D covariance matrices, 6 floats per splat: the upper triangle\n * (m00, m01, m02, m11, m12, m22) of the symmetric 3×3 matrix\n * Σ = R·S·Sᵀ·Rᵀ, precomputed from the per-splat scale and rotation.\n */\n readonly covariances: Float32Array;\n /** Higher-order spherical harmonics (view-dependent color), if present. */\n readonly sh?: SplatShData;\n /**\n * Per-splat (rather than palette-compressed) higher-order SH, as LCC `Quality`\n * delivers it. Unlike {@link sh}, this survives being appended into a\n * shared pool, because there is no per-file palette to merge.\n */\n readonly shPacked?: SplatPackedShData;\n /**\n * Scene was trained/exported with antialiasing (e.g. the SOG `antialias`\n * meta flag). When true, {@link SplatMesh} applies the Mip-Splatting 2D\n * filter (dilation + opacity compensation). Absent/false = classic 3DGS.\n */\n readonly antialias?: boolean;\n /**\n * Per-splat LOD-tree links for a streamed `.rad` chunk (the merged coarse\n * nodes and leaves are all present in {@link positions} etc.). The streamed\n * reader uses this to pick the rendered cut across chunks; other formats and\n * the one-shot path leave it undefined. See `parse-rad.ts` and `rad.ts`.\n */\n readonly radTree?: RadTreeData;\n /**\n * Per-splat parent LOD-node size for the frontier cut (`foveationMode:\n * 'frontier'`), sign-encoding leaf-ness: `> 0` internal node, `< 0` leaf,\n * `|value|` is the parent's world size (a large sentinel ≈ ∞ for a root or a\n * splat whose parent is not yet decoded). Attached by `RadFoveatedSource` as\n * chunks stream in; uploaded into `covarianceB.w`. See `docs/formats/rad-notes.md`.\n */\n readonly frontierParent?: Float32Array;\n /** Internal RAD SH codebook retained while a streamed `.rad` scene is set\n * up. It is passed back to later chunk decodes; consumers can ignore it. */\n readonly radShCodebook?: RadShCodebook;\n /**\n * The self-contained format this scene was decoded from, stamped by the\n * loaders ({@link loadScene}/{@link loadSceneFile}). {@link SplatMesh} reads\n * it to pick the `orientation: 'y-up'` correction. Absent on hand-built data\n * and streamed chunks, in which case no orientation is ever inferred.\n */\n readonly sourceFormat?: SplatSourceFormat;\n}\n\n/** Palette emitted once in chunk zero by Spark's clustered RAD SH encoding. */\nexport interface RadShCodebook {\n readonly bands: 1 | 2 | 3;\n /** RGB triples, coefficient-major and codebook-entry-major. */\n readonly coefficients: Float32Array;\n readonly count: number;\n}\n\n/** Per-splat LOD-tree links carried on a streamed `.rad` chunk's `SplatData`. */\nexport interface RadTreeData {\n /** Number of children of splat `i`; 0 marks a leaf. */\n readonly childCount: Uint16Array;\n /** Global index of splat `i`'s first child; its children are the contiguous\n * range `[childStart, childStart + childCount)`. */\n readonly childStart: Uint32Array;\n /**\n * LOD node size (world units) of splat `i` - Spark's `2·expansion·avg(scale)`,\n * the radius the runtime cut projects to a screen size. Drives foveation\n * (see `docs/formats/rad-notes.md` M14.6): `pixel_scale = size / distance`.\n */\n readonly size: Float32Array;\n}\n\n/**\n * Palette-compressed higher-order SH coefficients, as in the SOG format:\n * each splat references one of up to 65,536 palette entries (its \"label\"),\n * and each entry holds the coefficients for all three color channels.\n */\nexport interface SplatShData {\n /** SH bands beyond the DC term: 1, 2 or 3 → 3, 8 or 15 coefficients. */\n readonly bands: number;\n /** Per-splat palette index, `count` entries. */\n readonly labels: Uint32Array;\n /**\n * Dequantized palette coefficients as an RGBA float image in the SOG\n * centroids layout: palette entry n, coefficient c lives at column\n * (n % 64) · coefficientCount + c, row ⌊n / 64⌋; the R, G, B channels\n * hold that coefficient for the red, green and blue SH respectively.\n * Uploadable directly as an RGBA32F texture.\n */\n readonly palette: Float32Array;\n readonly paletteWidth: number;\n readonly paletteHeight: number;\n}\n\n/**\n * Per-splat higher-order SH coefficients, still in their source packing:\n * one `uint32` per coefficient holding all three channels (R: bits 0-10,\n * G: bits 11-20, B: bits 21-31), dequantized across {@link range}.\n *\n * The words are kept packed all the way to the GPU - the shader unpacks them.\n * Expanding to floats would cost 45 floats per splat (~1.15 GB over a 6M-splat\n * pool) where the packed form costs 64 bytes (~384 MB) and is lossless.\n */\nexport interface SplatPackedShData {\n /** SH bands beyond the DC term: 1, 2 or 3 → 3, 8 or 15 coefficients. */\n readonly bands: 1 | 2 | 3;\n /** `count * coefficients` words, splat-major. */\n readonly packed: Uint32Array;\n /** Per-channel dequantization range shared by every coefficient. */\n readonly range: {\n readonly min: readonly [number, number, number];\n readonly max: readonly [number, number, number];\n };\n}\n\n/** First-order spherical harmonics constant: Y₀₀ = 1 / (2√π). */\nexport const SH_C0 = 0.28209479177387814;\n\n/**\n * Folds a splat's linear scale and rotation quaternion into the upper\n * triangle of its 3D covariance matrix Σ = R·S·Sᵀ·Rᵀ (the 3DGS paper's\n * parameterization), written to `out` at `index * 6`.\n *\n * The quaternion (w, x, y, z) does not need to be normalized.\n */\nexport function writeCovariance(\n out: Float32Array,\n index: number,\n scaleX: number,\n scaleY: number,\n scaleZ: number,\n qw: number,\n qx: number,\n qy: number,\n qz: number,\n): void {\n const qLength = Math.hypot(qw, qx, qy, qz) || 1;\n const w = qw / qLength;\n const x = qx / qLength;\n const y = qy / qLength;\n const z = qz / qLength;\n\n // M = R·S (rotation matrix from the quaternion, columns scaled).\n const m00 = (1 - 2 * (y * y + z * z)) * scaleX;\n const m01 = 2 * (x * y - w * z) * scaleY;\n const m02 = 2 * (x * z + w * y) * scaleZ;\n const m10 = 2 * (x * y + w * z) * scaleX;\n const m11 = (1 - 2 * (x * x + z * z)) * scaleY;\n const m12 = 2 * (y * z - w * x) * scaleZ;\n const m20 = 2 * (x * z - w * y) * scaleX;\n const m21 = 2 * (y * z + w * x) * scaleY;\n const m22 = (1 - 2 * (x * x + y * y)) * scaleZ;\n\n // Σ = M·Mᵀ is symmetric; store its upper triangle.\n out[index * 6 + 0] = m00 * m00 + m01 * m01 + m02 * m02;\n out[index * 6 + 1] = m00 * m10 + m01 * m11 + m02 * m12;\n out[index * 6 + 2] = m00 * m20 + m01 * m21 + m02 * m22;\n out[index * 6 + 3] = m10 * m10 + m11 * m11 + m12 * m12;\n out[index * 6 + 4] = m10 * m20 + m11 * m21 + m12 * m22;\n out[index * 6 + 5] = m20 * m20 + m21 * m21 + m22 * m22;\n}\n\n/**\n * Transforms an existing covariance Σ (upper triangle `m00,m01,m02,m11,m12,m22`)\n * by the 3×3 linear part `A` of a world matrix, writing Σ' = A·Σ·Aᵀ back as an\n * upper triangle. This is how a Gaussian's shape maps into a parent frame when a\n * splat cloud is placed with a world transform - the rotation/scale portion of\n * that transform (translation does not affect covariance) applied to the shape.\n *\n * Unlike {@link writeCovariance}, which *builds* Σ from a splat's scale and\n * rotation, this operates on an already-folded Σ. Non-uniform scale and shear in\n * `A` are handled exactly (the result is still a valid symmetric covariance).\n *\n * `a` is the linear part in **column-major** order - the first 9 elements of a\n * {@link https://threejs.org/docs/#api/en/math/Matrix4 | THREE.Matrix4}'s\n * `elements` layout for a matrix with no projective row, i.e.\n * `[a00, a10, a20, a01, a11, a21, a02, a12, a22]`. Reading `Matrix4.elements`\n * directly (ignoring the translation column and bottom row) yields exactly this.\n */\nexport function transformCovariance(\n out: Float32Array,\n outIndex: number,\n cov: Float32Array,\n covIndex: number,\n a: ArrayLike<number>,\n): void {\n // A, column-major: a[col*4 ... ] would be Matrix4; here we take the linear 3×3\n // as elements[0..2] = column 0, [3..5] would skip - but Matrix4 stores a 4×4,\n // so callers pass the compacted 9-element linear part described above.\n const a00 = a[0] as number;\n const a10 = a[1] as number;\n const a20 = a[2] as number;\n const a01 = a[3] as number;\n const a11 = a[4] as number;\n const a21 = a[5] as number;\n const a02 = a[6] as number;\n const a12 = a[7] as number;\n const a22 = a[8] as number;\n\n // Σ (symmetric) from the upper triangle.\n const s00 = cov[covIndex * 6 + 0] as number;\n const s01 = cov[covIndex * 6 + 1] as number;\n const s02 = cov[covIndex * 6 + 2] as number;\n const s11 = cov[covIndex * 6 + 3] as number;\n const s12 = cov[covIndex * 6 + 4] as number;\n const s22 = cov[covIndex * 6 + 5] as number;\n\n // T = A·Σ (3×3). Rows of A dotted with columns of the symmetric Σ.\n const t00 = a00 * s00 + a01 * s01 + a02 * s02;\n const t01 = a00 * s01 + a01 * s11 + a02 * s12;\n const t02 = a00 * s02 + a01 * s12 + a02 * s22;\n const t10 = a10 * s00 + a11 * s01 + a12 * s02;\n const t11 = a10 * s01 + a11 * s11 + a12 * s12;\n const t12 = a10 * s02 + a11 * s12 + a12 * s22;\n const t20 = a20 * s00 + a21 * s01 + a22 * s02;\n const t21 = a20 * s01 + a21 * s11 + a22 * s12;\n const t22 = a20 * s02 + a21 * s12 + a22 * s22;\n\n // Σ' = T·Aᵀ; store the upper triangle (Σ' is symmetric).\n out[outIndex * 6 + 0] = t00 * a00 + t01 * a01 + t02 * a02;\n out[outIndex * 6 + 1] = t00 * a10 + t01 * a11 + t02 * a12;\n out[outIndex * 6 + 2] = t00 * a20 + t01 * a21 + t02 * a22;\n out[outIndex * 6 + 3] = t10 * a10 + t11 * a11 + t12 * a12;\n out[outIndex * 6 + 4] = t10 * a20 + t11 * a21 + t12 * a22;\n out[outIndex * 6 + 5] = t20 * a20 + t21 * a21 + t22 * a22;\n}\n"],"names":["SH_C0","writeCovariance","out","index","scaleX","scaleY","scaleZ","qw","qx","qy","qz","qLength","w","x","y","z","m00","m01","m02","m10","m11","m12","m20","m21","m22"],"mappings":"AAiIO,MAAMA,IAAQ;AASd,SAASC,EACdC,GACAC,GACAC,GACAC,GACAC,GACAC,GACAC,GACAC,GACAC,GACM;AACN,QAAMC,IAAU,KAAK,MAAMJ,GAAIC,GAAIC,GAAIC,CAAE,KAAK,GACxCE,IAAIL,IAAKI,GACTE,IAAIL,IAAKG,GACTG,IAAIL,IAAKE,GACTI,IAAIL,IAAKC,GAGTK,KAAO,IAAI,KAAKF,IAAIA,IAAIC,IAAIA,MAAMX,GAClCa,IAAM,KAAKJ,IAAIC,IAAIF,IAAIG,KAAKV,GAC5Ba,IAAM,KAAKL,IAAIE,IAAIH,IAAIE,KAAKR,GAC5Ba,IAAM,KAAKN,IAAIC,IAAIF,IAAIG,KAAKX,GAC5BgB,KAAO,IAAI,KAAKP,IAAIA,IAAIE,IAAIA,MAAMV,GAClCgB,IAAM,KAAKP,IAAIC,IAAIH,IAAIC,KAAKP,GAC5BgB,IAAM,KAAKT,IAAIE,IAAIH,IAAIE,KAAKV,GAC5BmB,IAAM,KAAKT,IAAIC,IAAIH,IAAIC,KAAKR,GAC5BmB,KAAO,IAAI,KAAKX,IAAIA,IAAIC,IAAIA,MAAMR;AAGxC,EAAAJ,EAAIC,IAAQ,IAAI,CAAC,IAAIa,IAAMA,IAAMC,IAAMA,IAAMC,IAAMA,GACnDhB,EAAIC,IAAQ,IAAI,CAAC,IAAIa,IAAMG,IAAMF,IAAMG,IAAMF,IAAMG,GACnDnB,EAAIC,IAAQ,IAAI,CAAC,IAAIa,IAAMM,IAAML,IAAMM,IAAML,IAAMM,GACnDtB,EAAIC,IAAQ,IAAI,CAAC,IAAIgB,IAAMA,IAAMC,IAAMA,IAAMC,IAAMA,GACnDnB,EAAIC,IAAQ,IAAI,CAAC,IAAIgB,IAAMG,IAAMF,IAAMG,IAAMF,IAAMG,GACnDtB,EAAIC,IAAQ,IAAI,CAAC,IAAImB,IAAMA,IAAMC,IAAMA,IAAMC,IAAMA;AACrD;"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { SplatSourceFormat } from './loading';
|
|
2
|
+
/**
|
|
3
|
+
* CPU-side representation of a set of 3D Gaussians, ready for GPU upload.
|
|
4
|
+
*
|
|
5
|
+
* Everything is stored in flat typed arrays (struct-of-arrays) so the data
|
|
6
|
+
* can be copied straight into textures and transferred to workers.
|
|
7
|
+
*/
|
|
8
|
+
export interface SplatData {
|
|
9
|
+
/** Number of Gaussians. */
|
|
10
|
+
readonly count: number;
|
|
11
|
+
/** Gaussian centers, 3 floats (x, y, z) per splat. */
|
|
12
|
+
readonly positions: Float32Array;
|
|
13
|
+
/** Base color + opacity, 4 bytes (r, g, b, a) per splat. */
|
|
14
|
+
readonly colors: Uint8Array;
|
|
15
|
+
/**
|
|
16
|
+
* 3D covariance matrices, 6 floats per splat: the upper triangle
|
|
17
|
+
* (m00, m01, m02, m11, m12, m22) of the symmetric 3×3 matrix
|
|
18
|
+
* Σ = R·S·Sᵀ·Rᵀ, precomputed from the per-splat scale and rotation.
|
|
19
|
+
*/
|
|
20
|
+
readonly covariances: Float32Array;
|
|
21
|
+
/** Higher-order spherical harmonics (view-dependent color), if present. */
|
|
22
|
+
readonly sh?: SplatShData;
|
|
23
|
+
/**
|
|
24
|
+
* Per-splat (rather than palette-compressed) higher-order SH, as LCC `Quality`
|
|
25
|
+
* delivers it. Unlike {@link sh}, this survives being appended into a
|
|
26
|
+
* shared pool, because there is no per-file palette to merge.
|
|
27
|
+
*/
|
|
28
|
+
readonly shPacked?: SplatPackedShData;
|
|
29
|
+
/**
|
|
30
|
+
* Scene was trained/exported with antialiasing (e.g. the SOG `antialias`
|
|
31
|
+
* meta flag). When true, {@link SplatMesh} applies the Mip-Splatting 2D
|
|
32
|
+
* filter (dilation + opacity compensation). Absent/false = classic 3DGS.
|
|
33
|
+
*/
|
|
34
|
+
readonly antialias?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Per-splat LOD-tree links for a streamed `.rad` chunk (the merged coarse
|
|
37
|
+
* nodes and leaves are all present in {@link positions} etc.). The streamed
|
|
38
|
+
* reader uses this to pick the rendered cut across chunks; other formats and
|
|
39
|
+
* the one-shot path leave it undefined. See `parse-rad.ts` and `rad.ts`.
|
|
40
|
+
*/
|
|
41
|
+
readonly radTree?: RadTreeData;
|
|
42
|
+
/**
|
|
43
|
+
* Per-splat parent LOD-node size for the frontier cut (`foveationMode:
|
|
44
|
+
* 'frontier'`), sign-encoding leaf-ness: `> 0` internal node, `< 0` leaf,
|
|
45
|
+
* `|value|` is the parent's world size (a large sentinel ≈ ∞ for a root or a
|
|
46
|
+
* splat whose parent is not yet decoded). Attached by `RadFoveatedSource` as
|
|
47
|
+
* chunks stream in; uploaded into `covarianceB.w`. See `docs/formats/rad-notes.md`.
|
|
48
|
+
*/
|
|
49
|
+
readonly frontierParent?: Float32Array;
|
|
50
|
+
/** Internal RAD SH codebook retained while a streamed `.rad` scene is set
|
|
51
|
+
* up. It is passed back to later chunk decodes; consumers can ignore it. */
|
|
52
|
+
readonly radShCodebook?: RadShCodebook;
|
|
53
|
+
/**
|
|
54
|
+
* The self-contained format this scene was decoded from, stamped by the
|
|
55
|
+
* loaders ({@link loadScene}/{@link loadSceneFile}). {@link SplatMesh} reads
|
|
56
|
+
* it to pick the `orientation: 'y-up'` correction. Absent on hand-built data
|
|
57
|
+
* and streamed chunks, in which case no orientation is ever inferred.
|
|
58
|
+
*/
|
|
59
|
+
readonly sourceFormat?: SplatSourceFormat;
|
|
60
|
+
}
|
|
61
|
+
/** Palette emitted once in chunk zero by Spark's clustered RAD SH encoding. */
|
|
62
|
+
export interface RadShCodebook {
|
|
63
|
+
readonly bands: 1 | 2 | 3;
|
|
64
|
+
/** RGB triples, coefficient-major and codebook-entry-major. */
|
|
65
|
+
readonly coefficients: Float32Array;
|
|
66
|
+
readonly count: number;
|
|
67
|
+
}
|
|
68
|
+
/** Per-splat LOD-tree links carried on a streamed `.rad` chunk's `SplatData`. */
|
|
69
|
+
export interface RadTreeData {
|
|
70
|
+
/** Number of children of splat `i`; 0 marks a leaf. */
|
|
71
|
+
readonly childCount: Uint16Array;
|
|
72
|
+
/** Global index of splat `i`'s first child; its children are the contiguous
|
|
73
|
+
* range `[childStart, childStart + childCount)`. */
|
|
74
|
+
readonly childStart: Uint32Array;
|
|
75
|
+
/**
|
|
76
|
+
* LOD node size (world units) of splat `i` - Spark's `2·expansion·avg(scale)`,
|
|
77
|
+
* the radius the runtime cut projects to a screen size. Drives foveation
|
|
78
|
+
* (see `docs/formats/rad-notes.md` M14.6): `pixel_scale = size / distance`.
|
|
79
|
+
*/
|
|
80
|
+
readonly size: Float32Array;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Palette-compressed higher-order SH coefficients, as in the SOG format:
|
|
84
|
+
* each splat references one of up to 65,536 palette entries (its "label"),
|
|
85
|
+
* and each entry holds the coefficients for all three color channels.
|
|
86
|
+
*/
|
|
87
|
+
export interface SplatShData {
|
|
88
|
+
/** SH bands beyond the DC term: 1, 2 or 3 → 3, 8 or 15 coefficients. */
|
|
89
|
+
readonly bands: number;
|
|
90
|
+
/** Per-splat palette index, `count` entries. */
|
|
91
|
+
readonly labels: Uint32Array;
|
|
92
|
+
/**
|
|
93
|
+
* Dequantized palette coefficients as an RGBA float image in the SOG
|
|
94
|
+
* centroids layout: palette entry n, coefficient c lives at column
|
|
95
|
+
* (n % 64) · coefficientCount + c, row ⌊n / 64⌋; the R, G, B channels
|
|
96
|
+
* hold that coefficient for the red, green and blue SH respectively.
|
|
97
|
+
* Uploadable directly as an RGBA32F texture.
|
|
98
|
+
*/
|
|
99
|
+
readonly palette: Float32Array;
|
|
100
|
+
readonly paletteWidth: number;
|
|
101
|
+
readonly paletteHeight: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Per-splat higher-order SH coefficients, still in their source packing:
|
|
105
|
+
* one `uint32` per coefficient holding all three channels (R: bits 0-10,
|
|
106
|
+
* G: bits 11-20, B: bits 21-31), dequantized across {@link range}.
|
|
107
|
+
*
|
|
108
|
+
* The words are kept packed all the way to the GPU - the shader unpacks them.
|
|
109
|
+
* Expanding to floats would cost 45 floats per splat (~1.15 GB over a 6M-splat
|
|
110
|
+
* pool) where the packed form costs 64 bytes (~384 MB) and is lossless.
|
|
111
|
+
*/
|
|
112
|
+
export interface SplatPackedShData {
|
|
113
|
+
/** SH bands beyond the DC term: 1, 2 or 3 → 3, 8 or 15 coefficients. */
|
|
114
|
+
readonly bands: 1 | 2 | 3;
|
|
115
|
+
/** `count * coefficients` words, splat-major. */
|
|
116
|
+
readonly packed: Uint32Array;
|
|
117
|
+
/** Per-channel dequantization range shared by every coefficient. */
|
|
118
|
+
readonly range: {
|
|
119
|
+
readonly min: readonly [number, number, number];
|
|
120
|
+
readonly max: readonly [number, number, number];
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** First-order spherical harmonics constant: Y₀₀ = 1 / (2√π). */
|
|
124
|
+
export declare const SH_C0 = 0.28209479177387814;
|
|
125
|
+
/**
|
|
126
|
+
* Folds a splat's linear scale and rotation quaternion into the upper
|
|
127
|
+
* triangle of its 3D covariance matrix Σ = R·S·Sᵀ·Rᵀ (the 3DGS paper's
|
|
128
|
+
* parameterization), written to `out` at `index * 6`.
|
|
129
|
+
*
|
|
130
|
+
* The quaternion (w, x, y, z) does not need to be normalized.
|
|
131
|
+
*/
|
|
132
|
+
export declare function writeCovariance(out: Float32Array, index: number, scaleX: number, scaleY: number, scaleZ: number, qw: number, qx: number, qy: number, qz: number): void;
|
|
133
|
+
/**
|
|
134
|
+
* Transforms an existing covariance Σ (upper triangle `m00,m01,m02,m11,m12,m22`)
|
|
135
|
+
* by the 3×3 linear part `A` of a world matrix, writing Σ' = A·Σ·Aᵀ back as an
|
|
136
|
+
* upper triangle. This is how a Gaussian's shape maps into a parent frame when a
|
|
137
|
+
* splat cloud is placed with a world transform - the rotation/scale portion of
|
|
138
|
+
* that transform (translation does not affect covariance) applied to the shape.
|
|
139
|
+
*
|
|
140
|
+
* Unlike {@link writeCovariance}, which *builds* Σ from a splat's scale and
|
|
141
|
+
* rotation, this operates on an already-folded Σ. Non-uniform scale and shear in
|
|
142
|
+
* `A` are handled exactly (the result is still a valid symmetric covariance).
|
|
143
|
+
*
|
|
144
|
+
* `a` is the linear part in **column-major** order - the first 9 elements of a
|
|
145
|
+
* {@link https://threejs.org/docs/#api/en/math/Matrix4 | THREE.Matrix4}'s
|
|
146
|
+
* `elements` layout for a matrix with no projective row, i.e.
|
|
147
|
+
* `[a00, a10, a20, a01, a11, a21, a02, a12, a22]`. Reading `Matrix4.elements`
|
|
148
|
+
* directly (ignoring the translation column and bottom row) yields exactly this.
|
|
149
|
+
*/
|
|
150
|
+
export declare function transformCovariance(out: Float32Array, outIndex: number, cov: Float32Array, covIndex: number, a: ArrayLike<number>): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 24-bit RGB packing for linear view-space depth used by GPU splat picking.
|
|
3
|
+
*
|
|
4
|
+
* Depth is stored as a normalized value in [0, 1] mapped across the camera
|
|
5
|
+
* near/far range, then quantized into three 8-bit channels (R = high byte).
|
|
6
|
+
* This matches an RGBA8 render target without a depth-texture readback path.
|
|
7
|
+
*/
|
|
8
|
+
import * as THREE from 'three/webgpu';
|
|
9
|
+
/**
|
|
10
|
+
* Packs a normalized depth in [0, 1] into 8-bit RGB (high → low).
|
|
11
|
+
* Values outside [0, 1] are clamped.
|
|
12
|
+
*/
|
|
13
|
+
export declare function packNormalizedDepth(normalized: number): {
|
|
14
|
+
r: number;
|
|
15
|
+
g: number;
|
|
16
|
+
b: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Unpacks 8-bit RGB channels back to a normalized depth in [0, 1].
|
|
20
|
+
*/
|
|
21
|
+
export declare function unpackNormalizedDepth(r: number, g: number, b: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* Maps a positive linear view-space depth (OpenGL: −z) into [0, 1] using the
|
|
24
|
+
* camera near/far plane. Clamps to the range.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeViewDepth(viewDepth: number, near: number, far: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Inverse of {@link normalizeViewDepth}.
|
|
29
|
+
*/
|
|
30
|
+
export declare function denormalizeViewDepth(normalized: number, near: number, far: number): number;
|
|
31
|
+
/**
|
|
32
|
+
* Reconstructs a world-space hit from NDC and a positive view-space depth
|
|
33
|
+
* (−z in OpenGL camera space). Writes into `outPoint` and returns camera
|
|
34
|
+
* distance. The camera must have an up-to-date world matrix.
|
|
35
|
+
*/
|
|
36
|
+
export declare function unprojectViewDepth(ndcX: number, ndcY: number, viewDepth: number, camera: THREE.Camera, outPoint: THREE.Vector3): {
|
|
37
|
+
point: THREE.Vector3;
|
|
38
|
+
distance: number;
|
|
39
|
+
};
|