holosplat 0.6.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/README.md +890 -0
- package/bin/holosplat.cjs +374 -0
- package/dist/holosplat.esm.js +766 -0
- package/dist/holosplat.esm.js.map +7 -0
- package/dist/holosplat.iife.js +766 -0
- package/dist/holosplat.iife.js.map +7 -0
- package/holosplat/editor.js +2947 -0
- package/holosplat/index.html +614 -0
- package/holosplat/stats.js +101 -0
- package/package.json +30 -0
- package/server.py +560 -0
- package/src/server.js +198 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
var Kt=`
|
|
2
|
+
|
|
3
|
+
struct Uniforms {
|
|
4
|
+
view : mat4x4<f32>,
|
|
5
|
+
proj : mat4x4<f32>,
|
|
6
|
+
viewport : vec2<f32>,
|
|
7
|
+
focal : vec2<f32>,
|
|
8
|
+
params : vec4<f32>, // .x = splatScale .y = near .z = gamma .w = radiusCap
|
|
9
|
+
shParams : vec4<f32>, // .x = shDegree .y = numSHBases .z = aaDilation
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
struct Gaussian {
|
|
13
|
+
pos : vec3<f32>,
|
|
14
|
+
part : f32, // part index (0.0, 1.0, 2.0\u2026) \u2014 cast to u32 in shader
|
|
15
|
+
color : vec4<f32>, // DC-only base color (0.5 + SH_C0 * f_dc)
|
|
16
|
+
scale : vec3<f32>,
|
|
17
|
+
quat : vec4<f32>,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
|
|
21
|
+
@group(0) @binding(1) var<storage, read> gaussians : array<Gaussian>;
|
|
22
|
+
@group(0) @binding(2) var<storage, read> order : array<u32>;
|
|
23
|
+
@group(0) @binding(3) var<storage, read> transforms : array<mat4x4<f32>>;
|
|
24
|
+
|
|
25
|
+
// Mask volume: a world-space box that clips splats \u2014 splats inside render at
|
|
26
|
+
// full size, splats outside fade to zero over a softEdge band.
|
|
27
|
+
struct MaskVolume {
|
|
28
|
+
invMatrix : mat4x4<f32>, // world \u2192 local transform (precomputed inverse)
|
|
29
|
+
params : vec4<f32>, // .x = softEdge (world-space fade width)
|
|
30
|
+
};
|
|
31
|
+
struct MaskUniforms {
|
|
32
|
+
header : vec4<u32>, // .x = active volume count (0..8)
|
|
33
|
+
volumes : array<MaskVolume, 8>,
|
|
34
|
+
};
|
|
35
|
+
@group(0) @binding(4) var<uniform> maskUnif : MaskUniforms;
|
|
36
|
+
@group(0) @binding(5) var<storage, read> partVolMask : array<u32>;
|
|
37
|
+
|
|
38
|
+
// SH rest coefficients \u2014 packed f32 array, no padding.
|
|
39
|
+
// Layout: for Gaussian gi, basis b: shCoeffs[(gi*numSHBases + b)*3 + channel].
|
|
40
|
+
// Bound to a tiny dummy buffer when shDegree == 0.
|
|
41
|
+
@group(0) @binding(6) var<storage, read> shCoeffs : array<f32>;
|
|
42
|
+
|
|
43
|
+
// TEMP debug instrumentation \u2014 writes intermediate per-splat values for the
|
|
44
|
+
// one gaussian index named in uniforms.shParams.w (\u22121 = disabled), so the
|
|
45
|
+
// GPU-computed math for a specific splat can be cross-checked on the CPU
|
|
46
|
+
// against hand-computed expected values from the raw source file.
|
|
47
|
+
@group(0) @binding(7) var<storage, read_write> debugOut : array<f32, 16>;
|
|
48
|
+
|
|
49
|
+
struct VOut {
|
|
50
|
+
@builtin(position) clipPos : vec4<f32>,
|
|
51
|
+
@location(0) color : vec4<f32>,
|
|
52
|
+
// Raw \xB11 quad-corner UV. The quad geometry itself is pre-warped to align
|
|
53
|
+
// with the 2-D covariance's eigenvectors (see cs_preprocess), so this UV
|
|
54
|
+
// directly parameterizes the ellipse \u2014 no per-pixel conic/Mahalanobis math
|
|
55
|
+
// needed.
|
|
56
|
+
@location(1) uv : vec2<f32>,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Per-splat results of the heavy per-gaussian math (covariance projection,
|
|
60
|
+
// eigendecomposition, SH), computed once per splat by cs_preprocess instead
|
|
61
|
+
// of once per *vertex* (6x redundant \u2014 every vertex of a splat's quad used
|
|
62
|
+
// to redo the same work). vs_main just reads this and places the quad.
|
|
63
|
+
// ndc.xy = screen-space center (NDC, pre-offset)
|
|
64
|
+
// ndc.z = NDC depth (clip.z/clip.w) \u2014 ndc.z > 1.5 marks a degenerate/
|
|
65
|
+
// discarded splat (near-plane, edge-on, etc.), matching the old
|
|
66
|
+
// degen() sentinel.
|
|
67
|
+
// v1/v2 = quad corner basis vectors (pixels), already scaled by sizeFade.
|
|
68
|
+
// color = final straight-alpha rgba (SH-evaluated rgb, alpha folded with
|
|
69
|
+
// nearFade/sizeFade/maskFade/aaFactor).
|
|
70
|
+
struct SplatGeom {
|
|
71
|
+
ndc : vec4<f32>,
|
|
72
|
+
v1 : vec2<f32>,
|
|
73
|
+
v2 : vec2<f32>,
|
|
74
|
+
color : vec4<f32>,
|
|
75
|
+
};
|
|
76
|
+
// read_write (compute-only) and read (vertex-only) views of the same buffer
|
|
77
|
+
// get distinct binding numbers \u2014 WGSL doesn't allow one module-scope
|
|
78
|
+
// resource to change access mode per entry point, and read_write storage
|
|
79
|
+
// isn't permitted outside the compute stage anyway.
|
|
80
|
+
@group(0) @binding(8) var<storage, read_write> splatGeomOut : array<SplatGeom>;
|
|
81
|
+
@group(0) @binding(9) var<storage, read> splatGeomIn : array<SplatGeom>;
|
|
82
|
+
|
|
83
|
+
struct PreprocessParams { count : vec4<u32> };
|
|
84
|
+
@group(0) @binding(10) var<uniform> ppParams : PreprocessParams;
|
|
85
|
+
|
|
86
|
+
// \u2500\u2500 SH constants (match ply-loader.js) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
87
|
+
const SH_C1 = 0.4886025119029199;
|
|
88
|
+
const SH_C2_0 = 1.0925484305920792;
|
|
89
|
+
const SH_C2_1 = -1.0925484305920792;
|
|
90
|
+
const SH_C2_2 = 0.31539156525252005;
|
|
91
|
+
const SH_C2_3 = -1.0925484305920792;
|
|
92
|
+
const SH_C2_4 = 0.5462742152960396;
|
|
93
|
+
const SH_C3_0 = -0.5900435899266435;
|
|
94
|
+
const SH_C3_1 = 2.890611442640554;
|
|
95
|
+
const SH_C3_2 = -0.4570457994644658;
|
|
96
|
+
const SH_C3_3 = 0.3731763325901154;
|
|
97
|
+
const SH_C3_4 = -0.4570457994644658;
|
|
98
|
+
const SH_C3_5 = 1.445305721320277;
|
|
99
|
+
const SH_C3_6 = -0.5900435899266435;
|
|
100
|
+
|
|
101
|
+
// Read one SH basis vector (3 packed floats) from the flat shCoeffs array.
|
|
102
|
+
// Each basis occupies 3 consecutive floats: [r, g, b].
|
|
103
|
+
fn shBasis(gi: u32, numBases: u32, b: u32) -> vec3<f32> {
|
|
104
|
+
let o = (gi * numBases + b) * 3u;
|
|
105
|
+
return vec3<f32>(shCoeffs[o], shCoeffs[o + 1u], shCoeffs[o + 2u]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Evaluate higher-order SH (degree 1\u20133) for Gaussian gi at direction dir.
|
|
109
|
+
// Returns the additive contribution to the DC color \u2014 caller clamps final sum.
|
|
110
|
+
fn evalSH(gi: u32, dir: vec3<f32>, shDeg: u32, numBases: u32) -> vec3<f32> {
|
|
111
|
+
var result = vec3<f32>(0.0);
|
|
112
|
+
if (shDeg == 0u) { return result; }
|
|
113
|
+
|
|
114
|
+
let x = dir.x; let y = dir.y; let z = dir.z;
|
|
115
|
+
|
|
116
|
+
let sh0 = shBasis(gi, numBases, 0u);
|
|
117
|
+
let sh1 = shBasis(gi, numBases, 1u);
|
|
118
|
+
let sh2 = shBasis(gi, numBases, 2u);
|
|
119
|
+
result += SH_C1 * (-sh0 * y + sh1 * z - sh2 * x);
|
|
120
|
+
if (shDeg == 1u) { return result; }
|
|
121
|
+
|
|
122
|
+
let sh3 = shBasis(gi, numBases, 3u);
|
|
123
|
+
let sh4 = shBasis(gi, numBases, 4u);
|
|
124
|
+
let sh5 = shBasis(gi, numBases, 5u);
|
|
125
|
+
let sh6 = shBasis(gi, numBases, 6u);
|
|
126
|
+
let sh7 = shBasis(gi, numBases, 7u);
|
|
127
|
+
let xx = x*x; let yy = y*y; let zz = z*z;
|
|
128
|
+
let xy = x*y; let xz = x*z; let yz = y*z;
|
|
129
|
+
result += SH_C2_0 * xy * sh3
|
|
130
|
+
+ SH_C2_1 * yz * sh4
|
|
131
|
+
+ SH_C2_2 * (2.0*zz - xx - yy) * sh5
|
|
132
|
+
+ SH_C2_3 * xz * sh6
|
|
133
|
+
+ SH_C2_4 * (xx - yy) * sh7;
|
|
134
|
+
if (shDeg == 2u) { return result; }
|
|
135
|
+
|
|
136
|
+
let sh8 = shBasis(gi, numBases, 8u);
|
|
137
|
+
let sh9 = shBasis(gi, numBases, 9u);
|
|
138
|
+
let sh10 = shBasis(gi, numBases, 10u);
|
|
139
|
+
let sh11 = shBasis(gi, numBases, 11u);
|
|
140
|
+
let sh12 = shBasis(gi, numBases, 12u);
|
|
141
|
+
let sh13 = shBasis(gi, numBases, 13u);
|
|
142
|
+
let sh14 = shBasis(gi, numBases, 14u);
|
|
143
|
+
result += SH_C3_0 * y * (3.0*xx - yy) * sh8
|
|
144
|
+
+ SH_C3_1 * xy * z * sh9
|
|
145
|
+
+ SH_C3_2 * y * (4.0*zz - xx - yy) * sh10
|
|
146
|
+
+ SH_C3_3 * z * (2.0*zz - 3.0*xx - 3.0*yy) * sh11
|
|
147
|
+
+ SH_C3_4 * x * (4.0*zz - xx - yy) * sh12
|
|
148
|
+
+ SH_C3_5 * (xx - yy) * z * sh13
|
|
149
|
+
+ SH_C3_6 * x * (xx - 3.0*yy) * sh14;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Quaternion (xyzw) \u2192 column-major rotation matrix
|
|
154
|
+
fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
155
|
+
let x = q.x; let y = q.y; let z = q.z; let w = q.w;
|
|
156
|
+
return mat3x3<f32>(
|
|
157
|
+
vec3<f32>(1.0 - 2.0*(y*y+z*z), 2.0*(x*y+w*z), 2.0*(x*z-w*y)),
|
|
158
|
+
vec3<f32>(2.0*(x*y-w*z), 1.0 - 2.0*(x*x+z*z), 2.0*(y*z+w*x)),
|
|
159
|
+
vec3<f32>(2.0*(x*z+w*y), 2.0*(y*z-w*x), 1.0 - 2.0*(x*x+y*y))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Writes the degenerate-splat sentinel (moved outside clip space \u2014 its quad
|
|
164
|
+
// gets clipped away entirely, matching the old per-vertex degen() early-out).
|
|
165
|
+
fn writeDegenerate(gi: u32) {
|
|
166
|
+
var o: SplatGeom;
|
|
167
|
+
o.ndc = vec4<f32>(0.0, 0.0, 2.0, 0.0);
|
|
168
|
+
o.v1 = vec2<f32>(0.0);
|
|
169
|
+
o.v2 = vec2<f32>(0.0);
|
|
170
|
+
o.color = vec4<f32>(0.0);
|
|
171
|
+
splatGeomOut[gi] = o;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// \u2500\u2500 Per-splat preprocess (compute) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
175
|
+
// Runs the heavy per-gaussian math \u2014 covariance projection, eigendecomposition,
|
|
176
|
+
// SH evaluation \u2014 exactly once per splat (not once per vertex). vs_main used
|
|
177
|
+
// to redo all of this 6x per splat (once per quad vertex); on weaker mobile
|
|
178
|
+
// GPUs that redundant vertex-shader work, not fragment/overdraw cost, turned
|
|
179
|
+
// out to be the dominant per-frame cost (lowering render resolution didn't
|
|
180
|
+
// help fps at all, which only makes sense if the bottleneck was vertex-bound).
|
|
181
|
+
@compute @workgroup_size(64)
|
|
182
|
+
fn cs_preprocess(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
183
|
+
let gi = gid.x;
|
|
184
|
+
if gi >= ppParams.count.x { return; }
|
|
185
|
+
|
|
186
|
+
let g = gaussians[gi];
|
|
187
|
+
|
|
188
|
+
// \u2500\u2500 Part transform: local \u2192 world \u2192 view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
189
|
+
let partId = u32(g.part);
|
|
190
|
+
let partMat = transforms[partId];
|
|
191
|
+
let worldPos4 = partMat * vec4<f32>(g.pos, 1.0);
|
|
192
|
+
let viewPos4 = uniforms.view * worldPos4;
|
|
193
|
+
let t = viewPos4.xyz;
|
|
194
|
+
|
|
195
|
+
// Discard if at or behind near plane
|
|
196
|
+
let near = uniforms.params.y;
|
|
197
|
+
if t.z > -near { writeDegenerate(gi); return; }
|
|
198
|
+
|
|
199
|
+
// Near-depth fade: fade out splats within 3\xD7 the near distance so that
|
|
200
|
+
// close-up splats don't cover the entire screen (matches Blender's behaviour).
|
|
201
|
+
let depth = -t.z;
|
|
202
|
+
let nearFade = clamp((depth - near) / (near * 2.0), 0.0, 1.0);
|
|
203
|
+
|
|
204
|
+
// \u2500\u2500 Mask volumes: fade splats outside assigned volume boxes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
205
|
+
let volBits = partVolMask[partId];
|
|
206
|
+
var maskFade = 1.0;
|
|
207
|
+
for (var vi = 0u; vi < maskUnif.header.x; vi++) {
|
|
208
|
+
if (((volBits >> vi) & 1u) != 0u) {
|
|
209
|
+
let vol = maskUnif.volumes[vi];
|
|
210
|
+
let lp = vol.invMatrix * worldPos4;
|
|
211
|
+
let q = abs(lp.xyz) - vec3<f32>(0.5);
|
|
212
|
+
let sdf = length(max(q, vec3<f32>(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0);
|
|
213
|
+
let e = max(vol.params.x, 0.001);
|
|
214
|
+
maskFade *= 1.0 - smoothstep(-e, e, sdf);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// \u2500\u2500 3-D covariance \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
219
|
+
let splatScale = uniforms.params.x;
|
|
220
|
+
let s = g.scale * (splatScale * maskFade);
|
|
221
|
+
let R = quatToMat3(g.quat);
|
|
222
|
+
// M = R * diag(s); Cov3D = M * M\u1D40
|
|
223
|
+
let M = mat3x3<f32>(R[0]*s.x, R[1]*s.y, R[2]*s.z);
|
|
224
|
+
let cov3 = M * transpose(M);
|
|
225
|
+
|
|
226
|
+
// \u2500\u2500 Project covariance to 2-D \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
227
|
+
// Perspective Jacobian at view-space point t (camera looks down \u2013Z)
|
|
228
|
+
let tz2 = t.z * t.z;
|
|
229
|
+
let fx = uniforms.focal.x;
|
|
230
|
+
let fy = uniforms.focal.y;
|
|
231
|
+
let J = mat3x3<f32>(
|
|
232
|
+
vec3<f32>(fx / (-t.z), 0.0, 0.0),
|
|
233
|
+
vec3<f32>(0.0, fy / (-t.z), 0.0),
|
|
234
|
+
vec3<f32>(fx*t.x / tz2, fy*t.y / tz2, 0.0)
|
|
235
|
+
);
|
|
236
|
+
// Combined rotation: view \xD7 part \u2014 maps splat-local covariance to view space.
|
|
237
|
+
// For single-part (identity partMat) this reduces to just the view rotation.
|
|
238
|
+
let R_part = mat3x3<f32>(partMat[0].xyz, partMat[1].xyz, partMat[2].xyz);
|
|
239
|
+
let W_view = mat3x3<f32>(uniforms.view[0].xyz, uniforms.view[1].xyz, uniforms.view[2].xyz);
|
|
240
|
+
let W = W_view * R_part;
|
|
241
|
+
let T = J * W;
|
|
242
|
+
let cov2 = T * cov3 * transpose(T);
|
|
243
|
+
|
|
244
|
+
// Extract 2\xD72 + low-pass filter (anti-aliasing dilation)
|
|
245
|
+
let aaDil = uniforms.shParams.z;
|
|
246
|
+
let detRaw = cov2[0][0] * cov2[1][1] - cov2[0][1] * cov2[0][1]; // pre-dilation det(\u03A3)
|
|
247
|
+
let a = cov2[0][0] + aaDil; // \u03A3_xx (cov2[col][row])
|
|
248
|
+
let b = cov2[0][1]; // \u03A3_xy
|
|
249
|
+
let c = cov2[1][1] + aaDil; // \u03A3_yy
|
|
250
|
+
|
|
251
|
+
let det = a*c - b*b;
|
|
252
|
+
if det < 1e-4 { writeDegenerate(gi); return; }
|
|
253
|
+
|
|
254
|
+
// Mip-Splatting opacity compensation: dilating the covariance for AA
|
|
255
|
+
// lowers the Gaussian's peak density (det grows), which silently dims
|
|
256
|
+
// every splat \u2014 small/thin splats (fine surface detail) the most, since
|
|
257
|
+
// aaDil is a larger fraction of their det. Rescale alpha by sqrt(det(\u03A3)/det(\u03A3+aaDil\xB7I))
|
|
258
|
+
// to preserve the splat's original peak opacity under the dilated kernel.
|
|
259
|
+
let aaFactor = sqrt(max(detRaw / det, 0.0));
|
|
260
|
+
|
|
261
|
+
// \u2500\u2500 Eigen-decomposition of the 2\xD72 covariance \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
262
|
+
// Quad geometry is aligned to the covariance's eigenvectors (matches
|
|
263
|
+
// PlayCanvas/SuperSplat's actual gsplat shader) so each splat rasterizes
|
|
264
|
+
// only the minimal area its ellipse actually covers \u2014 an axis-aligned
|
|
265
|
+
// isotropic bound (sized to the larger eigenvalue in both screen axes) was
|
|
266
|
+
// tried instead to dodge a suspected eigenvector-orientation instability,
|
|
267
|
+
// but that didn't fix anything and meaningfully increased overdraw on
|
|
268
|
+
// anisotropic splats (every elongated splat rasterizing a much bigger
|
|
269
|
+
// square than it needs), which is a likely contributor to the overall
|
|
270
|
+
// softness compared to SuperSplat.
|
|
271
|
+
//
|
|
272
|
+
// Degenerate (near edge-on, disk-like) splats \u2014 where the minor-axis
|
|
273
|
+
// variance goes non-positive \u2014 are discarded outright, matching
|
|
274
|
+
// GaussianSplats3D's eigenValue2 <= 0.0 early return, rather than forced
|
|
275
|
+
// to a minimum width. A previous version floored lambda2 to 0.1, which
|
|
276
|
+
// renders these as a faint but visible ~1px sliver instead of nothing \u2014
|
|
277
|
+
// on a curved/reflective surface, many splats sit near this grazing-angle
|
|
278
|
+
// case, and the accumulated phantom slivers are a plausible source of the
|
|
279
|
+
// "cloudy" look on specular surfaces specifically.
|
|
280
|
+
let mid = 0.5 * (a + c);
|
|
281
|
+
let evDist = length(vec2<f32>((a - c) * 0.5, b));
|
|
282
|
+
let lambda1 = mid + evDist;
|
|
283
|
+
let lambda2 = mid - evDist;
|
|
284
|
+
if lambda2 <= 0.0 { writeDegenerate(gi); return; }
|
|
285
|
+
|
|
286
|
+
let vmin = min(1024.0, min(uniforms.viewport.x, uniforms.viewport.y));
|
|
287
|
+
let l1 = 2.0 * min(sqrt(2.0 * lambda1), vmin);
|
|
288
|
+
let l2 = 2.0 * min(sqrt(2.0 * lambda2), vmin);
|
|
289
|
+
|
|
290
|
+
let eigenDir = normalize(vec2<f32>(b, lambda1 - a) + vec2<f32>(1e-6, 0.0));
|
|
291
|
+
let v1 = l1 * eigenDir;
|
|
292
|
+
let v2 = l2 * vec2<f32>(eigenDir.y, -eigenDir.x);
|
|
293
|
+
|
|
294
|
+
// Clamp the major-axis extent: splats larger than radiusCap \xD7 viewport
|
|
295
|
+
// height are faded out and capped, preventing a handful of close-up splats
|
|
296
|
+
// from overdrawing the whole screen. radiusCap shrinks adaptively under
|
|
297
|
+
// load \u2014 see Viewer#_updateAdaptiveQuality. l1/l2 are corner-to-center
|
|
298
|
+
// offset magnitudes (already radii, not diameters \u2014 see v1/v2 below).
|
|
299
|
+
let maxRadius = uniforms.viewport.y * uniforms.params.w;
|
|
300
|
+
let extent = max(l1, l2);
|
|
301
|
+
// Same factor both shrinks the quad geometry and fades its alpha \u2014 capped
|
|
302
|
+
// splats fade out as they're clamped, rather than abruptly losing their
|
|
303
|
+
// true aspect ratio.
|
|
304
|
+
let sizeFade = clamp(maxRadius / max(extent, 1.0), 0.0, 1.0);
|
|
305
|
+
|
|
306
|
+
// \u2500\u2500 View-dependent SH colour \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
307
|
+
// Camera world-space position from the view matrix:
|
|
308
|
+
// view = [R | -R*cam], so cam = -R\u1D40 * t where t = view[3].xyz
|
|
309
|
+
// In column-major WGSL: cam.x = -dot(view[0].xyz, view[3].xyz), etc.
|
|
310
|
+
let shDeg = u32(uniforms.shParams.x);
|
|
311
|
+
let shBases = u32(uniforms.shParams.y);
|
|
312
|
+
var rgb = g.color.rgb;
|
|
313
|
+
if (shDeg > 0u) {
|
|
314
|
+
let camWorld = vec3<f32>(
|
|
315
|
+
-dot(uniforms.view[0].xyz, uniforms.view[3].xyz),
|
|
316
|
+
-dot(uniforms.view[1].xyz, uniforms.view[3].xyz),
|
|
317
|
+
-dot(uniforms.view[2].xyz, uniforms.view[3].xyz)
|
|
318
|
+
);
|
|
319
|
+
// Reference convention (3DGS eval_sh): dir = point - camera (outward
|
|
320
|
+
// along the view ray), not camera - point. Degree-1/3 terms are odd in
|
|
321
|
+
// dir, so this sign was previously inverting half the SH contribution.
|
|
322
|
+
let shDir = normalize(worldPos4.xyz - camWorld);
|
|
323
|
+
rgb = rgb + evalSH(gi, shDir, shDeg, shBases);
|
|
324
|
+
}
|
|
325
|
+
// Single clamp after the full DC + SH-rest sum \u2014 g.color.rgb (the DC term)
|
|
326
|
+
// is intentionally left unclamped by the loader, since clamping it before
|
|
327
|
+
// adding the higher-order terms would bake in the wrong value.
|
|
328
|
+
rgb = clamp(rgb, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
329
|
+
|
|
330
|
+
let clip = uniforms.proj * viewPos4;
|
|
331
|
+
let ndcXY = clip.xy / clip.w;
|
|
332
|
+
|
|
333
|
+
var o: SplatGeom;
|
|
334
|
+
o.ndc = vec4<f32>(ndcXY, clip.z / clip.w, 0.0);
|
|
335
|
+
o.v1 = v1 * sizeFade;
|
|
336
|
+
o.v2 = v2 * sizeFade;
|
|
337
|
+
o.color = vec4<f32>(rgb, g.color.a * nearFade * sizeFade * maskFade * aaFactor);
|
|
338
|
+
splatGeomOut[gi] = o;
|
|
339
|
+
|
|
340
|
+
// TEMP debug instrumentation \u2014 see binding(7) declaration above. Writable
|
|
341
|
+
// directly here (compute allows read_write storage; the old vertex-shader
|
|
342
|
+
// version had to smuggle these out via varyings into fs_main instead).
|
|
343
|
+
if gi == u32(uniforms.shParams.w) {
|
|
344
|
+
debugOut[0] = t.z; debugOut[1] = a; debugOut[2] = b; debugOut[3] = c;
|
|
345
|
+
debugOut[4] = det; debugOut[5] = lambda1; debugOut[6] = lambda2; debugOut[7] = extent;
|
|
346
|
+
debugOut[8] = sizeFade; debugOut[9] = nearFade; debugOut[10] = aaFactor; debugOut[11] = o.color.a;
|
|
347
|
+
debugOut[12] = rgb.x; debugOut[13] = rgb.y; debugOut[14] = rgb.z; debugOut[15] = worldPos4.x;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@vertex
|
|
352
|
+
fn vs_main(
|
|
353
|
+
@builtin(vertex_index) vi : u32,
|
|
354
|
+
@builtin(instance_index) ii : u32,
|
|
355
|
+
) -> VOut {
|
|
356
|
+
// Quad corners (two triangles, CCW)
|
|
357
|
+
const corners = array<vec2<f32>, 6>(
|
|
358
|
+
vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0, -1.0), vec2<f32>(-1.0, 1.0),
|
|
359
|
+
vec2<f32>( 1.0, -1.0), vec2<f32>( 1.0, 1.0), vec2<f32>(-1.0, 1.0)
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
let gi = order[ii];
|
|
363
|
+
let geo = splatGeomIn[gi];
|
|
364
|
+
let corner = corners[vi];
|
|
365
|
+
|
|
366
|
+
var o: VOut;
|
|
367
|
+
if geo.ndc.z > 1.5 {
|
|
368
|
+
o.clipPos = vec4<f32>(0.0, 0.0, 2.0, 1.0);
|
|
369
|
+
o.color = vec4<f32>(0.0);
|
|
370
|
+
o.uv = vec2<f32>(0.0);
|
|
371
|
+
return o;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// corner (\xB11,\xB11) is split across the two eigenvector axes (v1, v2) instead
|
|
375
|
+
// of a single axis-aligned radius \u2014 this is what lets fs_main use a plain
|
|
376
|
+
// circular falloff over the unwarped corner UV.
|
|
377
|
+
let pixOff = corner.x * geo.v1 + corner.y * geo.v2;
|
|
378
|
+
let ndcOff = pixOff / (uniforms.viewport * 0.5);
|
|
379
|
+
|
|
380
|
+
o.clipPos = vec4<f32>(geo.ndc.xy + ndcOff, geo.ndc.z, 1.0);
|
|
381
|
+
o.color = geo.color;
|
|
382
|
+
o.uv = corner;
|
|
383
|
+
return o;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
@fragment
|
|
387
|
+
fn fs_main(in: VOut) -> @location(0) vec4<f32> {
|
|
388
|
+
// Plain Gaussian falloff \u2014 matches the original 3DGS reference renderer and
|
|
389
|
+
// mkkellogg/GaussianSplats3D (an independent, widely-used implementation),
|
|
390
|
+
// not PlayCanvas's hard-edged normExp, which turned out to be a
|
|
391
|
+
// PlayCanvas-specific stylistic choice rather than a more "correct" shape.
|
|
392
|
+
// in.uv (raw \xB11 quad corner) is built so dot(uv,uv)=1 at the edge
|
|
393
|
+
// corresponds to a true Mahalanobis\xB2 of 8 (since the quad extent is
|
|
394
|
+
// sqrt(8\xB7\u03BB) \u2014 see cs_preprocess), so the equivalent exp(-0.5\xB78\xB7A) = exp(-4\xB7A).
|
|
395
|
+
let A = dot(in.uv, in.uv);
|
|
396
|
+
if A > 1.0 { discard; }
|
|
397
|
+
let alpha = in.color.a * exp(-4.0 * A);
|
|
398
|
+
if alpha < 1.0/255.0 { discard; }
|
|
399
|
+
let gamma = max(uniforms.params.z, 0.01);
|
|
400
|
+
let rgb = pow(max(in.color.rgb, vec3<f32>(0.0)), vec3<f32>(1.0 / gamma));
|
|
401
|
+
return vec4<f32>(rgb * alpha, alpha);
|
|
402
|
+
}
|
|
403
|
+
`;var Be=`
|
|
404
|
+
|
|
405
|
+
struct Uniforms {
|
|
406
|
+
view : mat4x4<f32>,
|
|
407
|
+
proj : mat4x4<f32>,
|
|
408
|
+
viewport : vec2<f32>,
|
|
409
|
+
focal : vec2<f32>,
|
|
410
|
+
params : vec4<f32>,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
struct Gaussian {
|
|
414
|
+
pos : vec3<f32>,
|
|
415
|
+
part : f32,
|
|
416
|
+
color : vec4<f32>,
|
|
417
|
+
scale : vec3<f32>,
|
|
418
|
+
quat : vec4<f32>,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
struct SortParams {
|
|
422
|
+
numElements : u32,
|
|
423
|
+
passIndex : u32,
|
|
424
|
+
numWorkgroups : u32,
|
|
425
|
+
numChunks : u32,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
|
|
429
|
+
@group(0) @binding(1) var<storage, read> gaussians : array<Gaussian>;
|
|
430
|
+
@group(0) @binding(2) var<storage, read> transforms : array<mat4x4<f32>>;
|
|
431
|
+
@group(0) @binding(3) var<uniform> sortParams : SortParams;
|
|
432
|
+
@group(0) @binding(4) var<storage, read_write> keysA : array<u32>;
|
|
433
|
+
@group(0) @binding(5) var<storage, read_write> keysB : array<u32>;
|
|
434
|
+
@group(0) @binding(6) var<storage, read_write> idxA : array<u32>;
|
|
435
|
+
@group(0) @binding(7) var<storage, read_write> idxB : array<u32>;
|
|
436
|
+
@group(0) @binding(8) var<storage, read_write> histo : array<u32>;
|
|
437
|
+
@group(0) @binding(9) var<storage, read_write> blockPfx : array<u32>;
|
|
438
|
+
@group(0) @binding(10) var<storage, read_write> globalPfx : array<u32>;
|
|
439
|
+
@group(0) @binding(11) var<storage, read_write> chunkTotal : array<u32>;
|
|
440
|
+
@group(0) @binding(12) var<storage, read_write> chunkPfx : array<u32>;
|
|
441
|
+
|
|
442
|
+
// \u2500\u2500 Ping-pong helpers: pass even -> A is source, B is dest; pass odd -> reversed \u2500\u2500
|
|
443
|
+
|
|
444
|
+
fn readKey(i: u32, p: u32) -> u32 {
|
|
445
|
+
if (p % 2u == 0u) { return keysA[i]; } else { return keysB[i]; }
|
|
446
|
+
}
|
|
447
|
+
fn writeKey(i: u32, p: u32, v: u32) {
|
|
448
|
+
if (p % 2u == 0u) { keysB[i] = v; } else { keysA[i] = v; }
|
|
449
|
+
}
|
|
450
|
+
fn readIdx(i: u32, p: u32) -> u32 {
|
|
451
|
+
if (p % 2u == 0u) { return idxA[i]; } else { return idxB[i]; }
|
|
452
|
+
}
|
|
453
|
+
fn writeIdx(i: u32, p: u32, v: u32) {
|
|
454
|
+
if (p % 2u == 0u) { idxB[i] = v; } else { idxA[i] = v; }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// \u2500\u2500 cs_depth_key: seed keys/indices from view-space depth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
458
|
+
|
|
459
|
+
@compute @workgroup_size(256)
|
|
460
|
+
fn cs_depth_key(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
461
|
+
let i = gid.x;
|
|
462
|
+
if (i >= sortParams.numElements) { return; }
|
|
463
|
+
|
|
464
|
+
let g = gaussians[i];
|
|
465
|
+
let partId = u32(g.part);
|
|
466
|
+
let world = transforms[partId] * vec4<f32>(g.pos, 1.0);
|
|
467
|
+
let view = uniforms.view * world;
|
|
468
|
+
|
|
469
|
+
// Flip all bits of the (always-negative) IEEE-754 depth so ascending u32
|
|
470
|
+
// order matches back-to-front draw order - same trick as src/sorter.js.
|
|
471
|
+
let bits = bitcast<u32>(view.z);
|
|
472
|
+
keysA[i] = bits ^ 0xffffffffu;
|
|
473
|
+
idxA[i] = i;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// \u2500\u2500 cs_histogram: per-workgroup 256-bin histogram of the current byte \u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
477
|
+
|
|
478
|
+
var<workgroup> localHist: array<atomic<u32>, 256>;
|
|
479
|
+
|
|
480
|
+
@compute @workgroup_size(256)
|
|
481
|
+
fn cs_histogram(
|
|
482
|
+
@builtin(global_invocation_id) gid: vec3<u32>,
|
|
483
|
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
484
|
+
@builtin(workgroup_id) wgid: vec3<u32>,
|
|
485
|
+
) {
|
|
486
|
+
atomicStore(&localHist[lid.x], 0u);
|
|
487
|
+
workgroupBarrier();
|
|
488
|
+
|
|
489
|
+
let i = gid.x;
|
|
490
|
+
if (i < sortParams.numElements) {
|
|
491
|
+
let key = readKey(i, sortParams.passIndex);
|
|
492
|
+
let bin = (key >> (sortParams.passIndex * 8u)) & 0xffu;
|
|
493
|
+
atomicAdd(&localHist[bin], 1u);
|
|
494
|
+
}
|
|
495
|
+
workgroupBarrier();
|
|
496
|
+
|
|
497
|
+
histo[wgid.x * 256u + lid.x] = atomicLoad(&localHist[lid.x]);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// \u2500\u2500 cs_scan_reduce: per-chunk per-bin exclusive prefixes + chunk totals \u2500\u2500\u2500\u2500\u2500
|
|
501
|
+
//
|
|
502
|
+
// Workgroups of histogram blocks are grouped into chunks of 256. Each
|
|
503
|
+
// dispatched workgroup (one per chunk) computes, for its chunk, the
|
|
504
|
+
// chunk-local exclusive prefix sum of histo[] into blockPfx[] (relative to
|
|
505
|
+
// the chunk's first block) plus the chunk's total per bin (chunkTotal[]).
|
|
506
|
+
// Bounded to 256 iterations per thread regardless of scene size.
|
|
507
|
+
|
|
508
|
+
@compute @workgroup_size(256)
|
|
509
|
+
fn cs_scan_reduce(
|
|
510
|
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
511
|
+
@builtin(workgroup_id) wgid: vec3<u32>,
|
|
512
|
+
) {
|
|
513
|
+
let bin = lid.x;
|
|
514
|
+
let c = wgid.x;
|
|
515
|
+
let nwg = sortParams.numWorkgroups;
|
|
516
|
+
var running: u32 = 0u;
|
|
517
|
+
for (var j = 0u; j < 256u; j = j + 1u) {
|
|
518
|
+
let wg = c * 256u + j;
|
|
519
|
+
if (wg < nwg) {
|
|
520
|
+
blockPfx[wg * 256u + bin] = running;
|
|
521
|
+
running = running + histo[wg * 256u + bin];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
chunkTotal[c * 256u + bin] = running;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// \u2500\u2500 cs_scan_global: scan chunk totals + compute global per-bin offsets \u2500\u2500\u2500\u2500\u2500
|
|
528
|
+
//
|
|
529
|
+
// Single workgroup. Each thread (one per bin) scans chunkTotal across all
|
|
530
|
+
// chunks into chunkPfx (bounded to numChunks <= 256 iterations even for
|
|
531
|
+
// huge scenes), then thread 0 scans the 256 per-bin totals into globalPfx.
|
|
532
|
+
|
|
533
|
+
var<workgroup> binTotal: array<u32, 256>;
|
|
534
|
+
|
|
535
|
+
@compute @workgroup_size(256)
|
|
536
|
+
fn cs_scan_global(@builtin(local_invocation_id) lid: vec3<u32>) {
|
|
537
|
+
let bin = lid.x;
|
|
538
|
+
let numChunks = sortParams.numChunks;
|
|
539
|
+
var running: u32 = 0u;
|
|
540
|
+
for (var c = 0u; c < numChunks; c = c + 1u) {
|
|
541
|
+
chunkPfx[c * 256u + bin] = running;
|
|
542
|
+
running = running + chunkTotal[c * 256u + bin];
|
|
543
|
+
}
|
|
544
|
+
binTotal[bin] = running;
|
|
545
|
+
workgroupBarrier();
|
|
546
|
+
|
|
547
|
+
if (bin == 0u) {
|
|
548
|
+
var acc: u32 = 0u;
|
|
549
|
+
for (var b = 0u; b < 256u; b = b + 1u) {
|
|
550
|
+
globalPfx[b] = acc;
|
|
551
|
+
acc = acc + binTotal[b];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// \u2500\u2500 cs_scan_combine: fold chunk offsets into blockPfx \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
557
|
+
//
|
|
558
|
+
// blockPfx[] currently holds chunk-local exclusive prefixes (from
|
|
559
|
+
// cs_scan_reduce); add each block's chunk offset (chunkPfx) to make it
|
|
560
|
+
// globally-relative. One thread per (workgroup, bin) - O(1) each.
|
|
561
|
+
|
|
562
|
+
@compute @workgroup_size(256)
|
|
563
|
+
fn cs_scan_combine(
|
|
564
|
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
565
|
+
@builtin(workgroup_id) wgid: vec3<u32>,
|
|
566
|
+
) {
|
|
567
|
+
let bin = lid.x;
|
|
568
|
+
let wg = wgid.x;
|
|
569
|
+
let chunk = wg / 256u;
|
|
570
|
+
blockPfx[wg * 256u + bin] = blockPfx[wg * 256u + bin] + chunkPfx[chunk * 256u + bin];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// \u2500\u2500 cs_scatter: stable scatter to sorted positions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
574
|
+
|
|
575
|
+
var<workgroup> wgBin: array<u32, 256>;
|
|
576
|
+
var<workgroup> wgRank: array<u32, 256>;
|
|
577
|
+
var<workgroup> counts: array<u32, 256>;
|
|
578
|
+
|
|
579
|
+
@compute @workgroup_size(256)
|
|
580
|
+
fn cs_scatter(
|
|
581
|
+
@builtin(global_invocation_id) gid: vec3<u32>,
|
|
582
|
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
583
|
+
@builtin(workgroup_id) wgid: vec3<u32>,
|
|
584
|
+
) {
|
|
585
|
+
let i = gid.x;
|
|
586
|
+
let p = sortParams.passIndex;
|
|
587
|
+
let inBounds = i < sortParams.numElements;
|
|
588
|
+
|
|
589
|
+
var key: u32 = 0u;
|
|
590
|
+
var idx: u32 = 0u;
|
|
591
|
+
var bin: u32 = 256u; // sentinel for out-of-bounds lanes
|
|
592
|
+
if (inBounds) {
|
|
593
|
+
key = readKey(i, p);
|
|
594
|
+
idx = readIdx(i, p);
|
|
595
|
+
bin = (key >> (p * 8u)) & 0xffu;
|
|
596
|
+
}
|
|
597
|
+
wgBin[lid.x] = bin;
|
|
598
|
+
counts[lid.x] = 0u;
|
|
599
|
+
workgroupBarrier();
|
|
600
|
+
|
|
601
|
+
// Single-threaded, in-order pass over this workgroup's 256 lanes: assigns
|
|
602
|
+
// each element a stable rank among same-bin elements (required so each
|
|
603
|
+
// 8-bit pass preserves the ordering established by previous passes).
|
|
604
|
+
// counts lives in workgroup memory (zeroed above by all 256 lanes in
|
|
605
|
+
// parallel) rather than a per-invocation private array, which would be
|
|
606
|
+
// allocated 256x over on some mobile GPU drivers.
|
|
607
|
+
if (lid.x == 0u) {
|
|
608
|
+
for (var k = 0u; k < 256u; k = k + 1u) {
|
|
609
|
+
let b = wgBin[k];
|
|
610
|
+
if (b < 256u) {
|
|
611
|
+
wgRank[k] = counts[b];
|
|
612
|
+
counts[b] = counts[b] + 1u;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
workgroupBarrier();
|
|
617
|
+
|
|
618
|
+
if (inBounds) {
|
|
619
|
+
let dst = globalPfx[bin] + blockPfx[wgid.x * 256u + bin] + wgRank[lid.x];
|
|
620
|
+
writeKey(dst, p, key);
|
|
621
|
+
writeIdx(dst, p, idx);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
`;function Ut(a){return Math.ceil(a/256)}function Jt(a){return Math.ceil(Ut(a)/256)}var ms=`
|
|
625
|
+
@group(0) @binding(0) var hdrTex : texture_2d<f32>;
|
|
626
|
+
@group(0) @binding(1) var hdrSampler : sampler;
|
|
627
|
+
|
|
628
|
+
struct VOut {
|
|
629
|
+
@builtin(position) pos : vec4<f32>,
|
|
630
|
+
@location(0) uv : vec2<f32>,
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
@vertex
|
|
634
|
+
fn vs_main(@builtin(vertex_index) vi : u32) -> VOut {
|
|
635
|
+
const pos = array<vec2<f32>, 3>(
|
|
636
|
+
vec2<f32>(-1.0, -1.0), vec2<f32>(3.0, -1.0), vec2<f32>(-1.0, 3.0)
|
|
637
|
+
);
|
|
638
|
+
var o: VOut;
|
|
639
|
+
o.pos = vec4<f32>(pos[vi], 0.0, 1.0);
|
|
640
|
+
o.uv = (pos[vi] * vec2<f32>(0.5, -0.5)) + vec2<f32>(0.5, 0.5);
|
|
641
|
+
return o;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
@fragment
|
|
645
|
+
fn fs_main(in: VOut) -> @location(0) vec4<f32> {
|
|
646
|
+
return textureSample(hdrTex, hdrSampler, in.uv);
|
|
647
|
+
}
|
|
648
|
+
`,ps=0,_s=16,ke=32,Me=34,lt=36,kt=40,Ce=42,Ae=44,Dt=class{constructor(t,e){this.canvas=t,this.background=Fe(e),this.device=null,this.context=null,this.pipeline=null,this.bindGroup=null,this._mainModule=null,this._hdrFormat="rgba16float",this._hdrTexture=null,this._hdrView=null,this._blitPipeline=null,this._blitSampler=null,this._blitBindGroup=null,this._uniformBuf=null,this._gaussianBuf=null,this._orderBuf=null,this._transformBuf=null,this._maskVolBuf=null,this._partVolMaskBuf=null,this._splatGeomBuf=null,this._preprocessParamsBuf=null,this._preprocessPipeline=null,this._preprocessBindGroup=null,this._preprocessCountData=new Uint32Array(4),this._shBuf=null,this._shDummyBuf=null,this._shNumBases=0,this._keysBufA=null,this._keysBufB=null,this._idxBufB=null,this._histoBuf=null,this._blockPfxBuf=null,this._globalPfxBuf=null,this._chunkTotalBuf=null,this._chunkPfxBuf=null,this._sortParamsBuf=null,this._sortParamsStaging=null,this._sortParamsData=new Uint32Array(20),this._depthKeyPipeline=null,this._histogramPipeline=null,this._scanReducePipeline=null,this._scanGlobalPipeline=null,this._scanCombinePipeline=null,this._scatterPipeline=null,this._depthKeyBindGroup=null,this._histogramBindGroup=null,this._scanReduceBindGroup=null,this._scanGlobalBindGroup=null,this._scanCombineBindGroup=null,this._scatterBindGroup=null,this._gpuSortFailed=!1,this._uniforms=new Float32Array(Ae),this._uniformsU32=new Uint32Array(this._uniforms.buffer),this._uniforms[lt]=1.08,this._uniforms[lt+2]=1,this._uniforms[lt+3]=1,this._uniforms[Ce]=.3,this._numSplats=0,this._maskWarned=new Set}async init(){if(!navigator.gpu)throw new Error("WebGPU is not supported in this browser.");let t=await navigator.gpu.requestAdapter();if(!t)throw new Error("No WebGPU adapter found.");this.device=await t.requestDevice({requiredLimits:{maxStorageBufferBindingSize:t.limits.maxStorageBufferBindingSize,maxBufferSize:t.limits.maxBufferSize}}),this.device.addEventListener("uncapturederror",s=>{console.error("[HoloSplat] GPU error:",s.error),this._gpuSortFailed=!0}),this.device.lost.then(s=>{console.error("[HoloSplat] WebGPU device lost:",s.reason,s.message)}),this.context=this.canvas.getContext("webgpu"),this._format=navigator.gpu.getPreferredCanvasFormat(),this.context.configure({device:this.device,format:this._format,alphaMode:"premultiplied"}),this._createPipeline(),this._createBlitPipeline(),this._createSortPipelines(),this._createPreprocessPipeline(),this._uniformBuf=this._createBuffer(Ae*4,GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST),this._preprocessParamsBuf=this._createBuffer(16,GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST),this._shDummyBuf=this._createBuffer(12,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST),this._debugBuf=this._createBuffer(16*4,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC),this._debugReadBuf=this._createBuffer(16*4,GPUBufferUsage.MAP_READ|GPUBufferUsage.COPY_DST),this._uniforms[kt+3]=-1,this._globalPfxBuf=this._createBuffer(256*4,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST),this._sortParamsBuf=this._createBuffer(16,GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST),this._sortParamsStaging=this._createBuffer(16*5,GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST);let e=new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);this._transformBuf=this._createBuffer(64,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST),this.device.queue.writeBuffer(this._transformBuf,0,e),this._maskVolBuf=this._createBuffer(656,GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST),this._partVolMaskBuf=this._createBuffer(4,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST)}uploadGaussians(t,e){this._numSplats=e,this._gaussianBuf=this.device.createBuffer({size:t.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.device.queue.writeBuffer(this._gaussianBuf,0,t),this._orderBuf=this.device.createBuffer({size:e*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC}),this._splatGeomBuf=this.device.createBuffer({size:Math.max(e*48,48),usage:GPUBufferUsage.STORAGE});let s=n=>this.device.createBuffer({size:Math.max(n*4,4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});this._keysBufA=s(e),this._keysBufB=s(e),this._idxBufB=s(e);let i=Ut(e),r=Jt(e);this._histoBuf=s(i*256),this._blockPfxBuf=s(i*256),this._chunkTotalBuf=s(r*256),this._chunkPfxBuf=s(r*256),this._rebuildBindGroup(),this._rebuildPreprocessBindGroup(),this._rebuildSortBindGroups()}updateUniforms({view:t,proj:e,width:s,height:i,focal:r,near:n=.01,radiusCap:o}){let l=this._uniforms;l.set(t,ps),l.set(e,_s),l[ke]=s,l[ke+1]=i,l[Me]=r,l[Me+1]=r,l[lt+1]=n,o!=null&&(l[lt+3]=o),this.device.queue.writeBuffer(this._uniformBuf,0,l)}updateOrder(t,e){let s=Math.min(e,this._orderBuf.size/4,t.length);s<e&&console.warn(`[HoloSplat] updateOrder: requested count (${e}) exceeds available buffer/source size (${s}) \u2014 clamping this frame's sort update`),!(s<=0)&&this.device.queue.writeBuffer(this._orderBuf,0,t.buffer,0,s*4)}uploadTransforms(t){let e=new Float32Array(t.length*16);for(let s=0;s<t.length;s++)e.set(t[s],s*16);this._transformBuf=this._createBuffer(e.byteLength,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST),this.device.queue.writeBuffer(this._transformBuf,0,e),this._gaussianBuf&&(this._rebuildPreprocessBindGroup(),this._rebuildSortBindGroups())}updateTransforms(t){this._transformBuf&&this.device.queue.writeBuffer(this._transformBuf,0,t)}uploadPartVolumeMask(t){this._partVolMaskBuf=this._createBuffer(Math.max(t.byteLength,4),GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST),this.device.queue.writeBuffer(this._partVolMaskBuf,0,t),this._gaussianBuf&&this._rebuildPreprocessBindGroup()}updateMaskVolumes(t){let e=Math.min(t.length,8),s=new ArrayBuffer(656),i=new Uint32Array(s),r=new Float32Array(s);i[0]=e;for(let n=0;n<e;n++){let l=16+n*80>>2,c=t[n],u=te(c.matrix);u?r.set(u,l):this._maskWarned.has(c.name)||(this._maskWarned.add(c.name),console.warn(`[HoloSplat] mask volume "${c.name}" has a non-invertible (degenerate) transform \u2014 it will have no effect. Check that its Blender object has non-zero scale on all 3 axes.`)),r[l+16]=c.softEdge??.05}this.device.queue.writeBuffer(this._maskVolBuf,0,s)}patchGaussians(t,e){this._gaussianBuf&&this.device.queue.writeBuffer(this._gaussianBuf,e*64,t)}setSplatScale(t){this._uniforms[lt]=t}setGamma(t){this._uniforms[lt+2]=t}uploadSH(t,e){this._shBuf=null,this._shNumBases=e||0,this._uniforms[kt+1]=this._shNumBases,t&&this._shNumBases>0&&(this._shBuf=this._createBuffer(t.byteLength,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST),this.device.queue.writeBuffer(this._shBuf,0,t)),this._gaussianBuf&&this._rebuildPreprocessBindGroup()}allocateSH(t,e){this._shBuf=null,this._shNumBases=e||0,this._uniforms[kt+1]=this._shNumBases,this._shNumBases>0&&t>0&&(this._shBuf=this._createBuffer(t*this._shNumBases*12,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST)),this._gaussianBuf&&this._rebuildPreprocessBindGroup()}patchSH(t,e){this._shBuf&&t&&this.device.queue.writeBuffer(this._shBuf,e*this._shNumBases*12,t)}setShDegree(t){this._uniforms[kt]=t}setAaDilation(t){this._uniforms[Ce]=t}setDebugIndex(t){this._uniforms[kt+3]=t}async readDebug(){let t=this.device.createCommandEncoder();t.copyBufferToBuffer(this._debugBuf,0,this._debugReadBuf,0,16*4),this.device.queue.submit([t.finish()]),await this._debugReadBuf.mapAsync(GPUMapMode.READ);let e=new Float32Array(this._debugReadBuf.getMappedRange().slice(0));this._debugReadBuf.unmap();let[s,i,r,n,o,l,c,u,m,f,d,h,p,w,y,g]=e;return{tz:s,a:i,b:r,c:n,det:o,lambda1:l,lambda2:c,extent:u,sizeFade:m,nearFade:f,aaFactor:d,finalAlpha:h,rgb:[p,w,y],worldX:g}}setBackground(t){this.background=Fe(t)}preprocess(t){if(!t||!this._preprocessBindGroup)return;this._preprocessCountData[0]=t,this.device.queue.writeBuffer(this._preprocessParamsBuf,0,this._preprocessCountData);let e=this.device.createCommandEncoder(),s=e.beginComputePass();s.setPipeline(this._preprocessPipeline),s.setBindGroup(0,this._preprocessBindGroup),s.dispatchWorkgroups(Math.ceil(t/64)),s.end(),this.device.queue.submit([e.finish()])}draw(t=this._numSplats){if(!t||!this.bindGroup)return;this._ensureHdrTexture();let e=this.device.createCommandEncoder(),s=e.beginRenderPass({colorAttachments:[{view:this._hdrView,clearValue:this.background,loadOp:"clear",storeOp:"store"}]});s.setPipeline(this.pipeline),s.setBindGroup(0,this.bindGroup),s.draw(6,t,0,0),s.end();let i=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),loadOp:"load",storeOp:"store"}]});i.setPipeline(this._blitPipeline),i.setBindGroup(0,this._blitBindGroup),i.draw(3,1,0,0),i.end(),this.device.queue.submit([e.finish()])}_ensureHdrTexture(){let t=this.canvas.width,e=this.canvas.height;this._hdrTexture&&this._hdrTexture.width===t&&this._hdrTexture.height===e||(this._hdrTexture?.destroy(),this._hdrTexture=this.device.createTexture({size:[t,e],format:this._hdrFormat,usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),this._hdrView=this._hdrTexture.createView(),this._blitBindGroup=this.device.createBindGroup({layout:this._blitPipeline.getBindGroupLayout(0),entries:[{binding:0,resource:this._hdrView},{binding:1,resource:this._blitSampler}]}))}destroy(){this._hdrTexture?.destroy(),this._debugBuf?.destroy(),this._debugReadBuf?.destroy(),this._uniformBuf?.destroy(),this._gaussianBuf?.destroy(),this._orderBuf?.destroy(),this._transformBuf?.destroy(),this._maskVolBuf?.destroy(),this._partVolMaskBuf?.destroy(),this._keysBufA?.destroy(),this._keysBufB?.destroy(),this._idxBufB?.destroy(),this._histoBuf?.destroy(),this._blockPfxBuf?.destroy(),this._globalPfxBuf?.destroy(),this._chunkTotalBuf?.destroy(),this._chunkPfxBuf?.destroy(),this._sortParamsBuf?.destroy(),this._sortParamsStaging?.destroy(),this._shBuf?.destroy(),this._shDummyBuf?.destroy(),this._splatGeomBuf?.destroy(),this._preprocessParamsBuf?.destroy(),this.context?.unconfigure()}_createPipeline(){this._mainModule=this._mainModule??this.device.createShaderModule({code:Kt});let t=this._mainModule;this.pipeline=this.device.createRenderPipeline({layout:"auto",vertex:{module:t,entryPoint:"vs_main"},fragment:{module:t,entryPoint:"fs_main",targets:[{format:this._hdrFormat,blend:{color:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"},alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha",operation:"add"}}}]},primitive:{topology:"triangle-list",cullMode:"none"}})}_createBlitPipeline(){let t=this.device.createShaderModule({code:ms});this._blitPipeline=this.device.createRenderPipeline({layout:"auto",vertex:{module:t,entryPoint:"vs_main"},fragment:{module:t,entryPoint:"fs_main",targets:[{format:this._format}]},primitive:{topology:"triangle-list"}}),this._blitSampler=this.device.createSampler({magFilter:"linear",minFilter:"linear"})}_createBuffer(t,e){return this.device.createBuffer({size:t,usage:e})}_createPreprocessPipeline(){this._mainModule=this._mainModule??this.device.createShaderModule({code:Kt});let t=this._mainModule;this._preprocessPipeline=this.device.createComputePipeline({layout:"auto",compute:{module:t,entryPoint:"cs_preprocess"}})}_createSortPipelines(){let t=this.device.createShaderModule({code:Be}),e=s=>this.device.createComputePipeline({layout:"auto",compute:{module:t,entryPoint:s}});this._depthKeyPipeline=e("cs_depth_key"),this._histogramPipeline=e("cs_histogram"),this._scanReducePipeline=e("cs_scan_reduce"),this._scanGlobalPipeline=e("cs_scan_global"),this._scanCombinePipeline=e("cs_scan_combine"),this._scatterPipeline=e("cs_scatter")}_rebuildBindGroup(){this.bindGroup=this.device.createBindGroup({layout:this.pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:this._uniformBuf}},{binding:2,resource:{buffer:this._orderBuf}},{binding:9,resource:{buffer:this._splatGeomBuf}}]})}_rebuildPreprocessBindGroup(){this._preprocessBindGroup=this.device.createBindGroup({layout:this._preprocessPipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:this._uniformBuf}},{binding:1,resource:{buffer:this._gaussianBuf}},{binding:3,resource:{buffer:this._transformBuf}},{binding:4,resource:{buffer:this._maskVolBuf}},{binding:5,resource:{buffer:this._partVolMaskBuf}},{binding:6,resource:{buffer:this._shBuf??this._shDummyBuf}},{binding:7,resource:{buffer:this._debugBuf}},{binding:8,resource:{buffer:this._splatGeomBuf}},{binding:10,resource:{buffer:this._preprocessParamsBuf}}]})}_rebuildSortBindGroups(){let t={0:{buffer:this._uniformBuf},1:{buffer:this._gaussianBuf},2:{buffer:this._transformBuf},3:{buffer:this._sortParamsBuf},4:{buffer:this._keysBufA},5:{buffer:this._keysBufB},6:{buffer:this._orderBuf},7:{buffer:this._idxBufB},8:{buffer:this._histoBuf},9:{buffer:this._blockPfxBuf},10:{buffer:this._globalPfxBuf},11:{buffer:this._chunkTotalBuf},12:{buffer:this._chunkPfxBuf}},e=(s,i)=>this.device.createBindGroup({layout:s.getBindGroupLayout(0),entries:i.map(r=>({binding:r,resource:t[r]}))});this._depthKeyBindGroup=e(this._depthKeyPipeline,[0,1,2,3,4,6]),this._histogramBindGroup=e(this._histogramPipeline,[3,4,5,8]),this._scanReduceBindGroup=e(this._scanReducePipeline,[3,8,9,11]),this._scanGlobalBindGroup=e(this._scanGlobalPipeline,[3,10,11,12]),this._scanCombineBindGroup=e(this._scanCombinePipeline,[9,12]),this._scatterBindGroup=e(this._scatterPipeline,[3,4,5,6,7,9,10])}runGpuSort(t){if(!t)return;let e=Ut(t),s=Jt(t),i=this._sortParamsData;for(let r=0;r<5;r++){let n=r*4;i[n]=t,i[n+1]=r===0?0:r-1,i[n+2]=e,i[n+3]=s}this.device.queue.writeBuffer(this._sortParamsStaging,0,i);{let r=this.device.createCommandEncoder();r.copyBufferToBuffer(this._sortParamsStaging,0,this._sortParamsBuf,0,16);let n=r.beginComputePass();n.setPipeline(this._depthKeyPipeline),n.setBindGroup(0,this._depthKeyBindGroup),n.dispatchWorkgroups(e),n.end(),this.device.queue.submit([r.finish()])}for(let r=0;r<4;r++){let n=this.device.createCommandEncoder();n.copyBufferToBuffer(this._sortParamsStaging,(r+1)*16,this._sortParamsBuf,0,16);let o=n.beginComputePass();o.setPipeline(this._histogramPipeline),o.setBindGroup(0,this._histogramBindGroup),o.dispatchWorkgroups(e),o.end();let l=n.beginComputePass();l.setPipeline(this._scanReducePipeline),l.setBindGroup(0,this._scanReduceBindGroup),l.dispatchWorkgroups(s),l.end();let c=n.beginComputePass();c.setPipeline(this._scanGlobalPipeline),c.setBindGroup(0,this._scanGlobalBindGroup),c.dispatchWorkgroups(1),c.end();let u=n.beginComputePass();u.setPipeline(this._scanCombinePipeline),u.setBindGroup(0,this._scanCombineBindGroup),u.dispatchWorkgroups(e),u.end();let m=n.beginComputePass();m.setPipeline(this._scatterPipeline),m.setBindGroup(0,this._scatterBindGroup),m.dispatchWorkgroups(e),m.end(),this.device.queue.submit([n.finish()])}}async debugReadOrder(t){let e=t*4,s=this.device.createBuffer({size:e,usage:GPUBufferUsage.MAP_READ|GPUBufferUsage.COPY_DST}),i=this.device.createCommandEncoder();i.copyBufferToBuffer(this._orderBuf,0,s,0,e),this.device.queue.submit([i.finish()]),await s.mapAsync(GPUMapMode.READ);let r=new Uint32Array(s.getMappedRange()).slice();return s.unmap(),s.destroy(),r}};function te(a){let t=a[0],e=a[1],s=a[2],i=a[3],r=a[4],n=a[5],o=a[6],l=a[7],c=a[8],u=a[9],m=a[10],f=a[11],d=a[12],h=a[13],p=a[14],w=a[15],y=t*n-e*r,g=t*o-s*r,S=t*l-i*r,_=e*o-s*n,v=e*l-i*n,B=s*l-i*o,b=c*h-u*d,P=c*p-m*d,M=c*w-f*d,U=u*p-m*h,I=u*w-f*h,V=m*w-f*p,z=y*V-g*I+S*U+_*M-v*P+B*b;if(!z)return null;let T=1/z,F=new Float32Array(16);return F[0]=(n*V-o*I+l*U)*T,F[1]=(s*I-e*V-i*U)*T,F[2]=(h*B-p*v+w*_)*T,F[3]=(m*v-u*B-f*_)*T,F[4]=(o*M-r*V-l*P)*T,F[5]=(t*V-s*M+i*P)*T,F[6]=(p*S-d*B-w*g)*T,F[7]=(c*B-m*S+f*g)*T,F[8]=(r*I-n*M+l*b)*T,F[9]=(e*M-t*I-i*b)*T,F[10]=(d*v-h*S+w*y)*T,F[11]=(u*S-c*v-f*y)*T,F[12]=(n*P-r*U-o*b)*T,F[13]=(t*U-e*P+s*b)*T,F[14]=(h*g-d*_-p*y)*T,F[15]=(c*_-u*g+m*y)*T,F}function Fe(a){if(!a||a==="transparent")return{r:0,g:0,b:0,a:0};if(Array.isArray(a))return{r:a[0],g:a[1],b:a[2],a:a[3]??1};if(typeof a=="string"){let t=a.replace("#","");if(t.length===6)return{r:parseInt(t.slice(0,2),16)/255,g:parseInt(t.slice(2,4),16)/255,b:parseInt(t.slice(4,6),16)/255,a:1};if(t.length===8)return{r:parseInt(t.slice(0,2),16)/255,g:parseInt(t.slice(2,4),16)/255,b:parseInt(t.slice(4,6),16)/255,a:parseInt(t.slice(6,8),16)/255}}return{r:0,g:0,b:0,a:1}}var Rt=class{constructor({fov:t=60,near:e=.01,far:s=2e3}={}){this.fov=t*Math.PI/180,this.near=e,this.far=s,this.theta=0,this.phi=.2,this.radius=5,this.target=[0,0,0],this.enabled=!0,this.orbitEnabled=!0,this.panEnabled=!0,this.panSpeed=1,this.panButton=2,this.panRadius=null,this.panOrigin=null,this.thetaMin=null,this.thetaMax=null,this.phiMin=null,this.phiMax=null,this.zoomEnabled=!0,this.radiusMin=null,this.radiusMax=null,this.orbitDeltaCallback=null,this.dragStartCallback=null,this.dragEndCallback=null,this.panDeltaCallback=null,this.zoomDeltaCallback=null,this._drag=null,this._touches=[],this._allowTouchScroll=!1,this.viewMatrix=new Float32Array(16),this.projMatrix=new Float32Array(16)}get allowTouchScroll(){return this._allowTouchScroll}set allowTouchScroll(t){this._allowTouchScroll=!!t,typeof document<"u"&&(document.documentElement.style.touchAction=t?"pan-y":"")}attach(t){this._canvas=t;let e=(s,i)=>t.contains(document.elementFromPoint(s,i));this._onMouseDown=s=>{e(s.clientX,s.clientY)&&this._mouseDown(s)},this._onMouseMove=s=>this._mouseMove(s),this._onMouseUp=()=>{this._drag=null,this.dragEndCallback?.()},this._onWheel=s=>this._wheel(s),this._onTouchStart=s=>{e(s.touches[0]?.clientX,s.touches[0]?.clientY)&&this._touchStart(s)},this._onTouchMove=s=>this._touchMove(s),this._onTouchEnd=s=>this._touchEnd(s),this._onCtxMenu=s=>{e(s.clientX,s.clientY)&&s.preventDefault()},this._onMouseOutDoc=s=>{s.relatedTarget===null&&this._drag&&(this._drag=null,this.dragEndCallback?.())},document.addEventListener("mousedown",this._onMouseDown),document.addEventListener("mousemove",this._onMouseMove),document.addEventListener("mouseup",this._onMouseUp),document.addEventListener("mouseout",this._onMouseOutDoc),t.addEventListener("wheel",this._onWheel,{passive:!1}),document.addEventListener("touchstart",this._onTouchStart,{passive:!1}),document.addEventListener("touchmove",this._onTouchMove,{passive:!1}),document.addEventListener("touchend",this._onTouchEnd),document.addEventListener("contextmenu",this._onCtxMenu)}detach(){let t=this._canvas;t&&(document.removeEventListener("mousedown",this._onMouseDown),document.removeEventListener("mousemove",this._onMouseMove),document.removeEventListener("mouseup",this._onMouseUp),document.removeEventListener("mouseout",this._onMouseOutDoc),t.removeEventListener("wheel",this._onWheel),document.removeEventListener("touchstart",this._onTouchStart),document.removeEventListener("touchmove",this._onTouchMove),document.removeEventListener("touchend",this._onTouchEnd),document.removeEventListener("contextmenu",this._onCtxMenu),this._canvas=null)}_mouseDown(t){this.enabled&&(t.button===2&&!this.panEnabled||(this._drag={x:t.clientX,y:t.clientY,button:t.button},t.button===0&&this.dragStartCallback?.(),t.preventDefault()))}_mouseMove(t){if(this._drag){let e=t.clientX-this._drag.x,s=t.clientY-this._drag.y;this._drag.x=t.clientX,this._drag.y=t.clientY;let i=this._drag.button===this.panButton;if(this.panEnabled&&(i||!this.orbitEnabled&&this._drag.button===0)){this._pan(e,s);return}this._drag.button===0&&!i&&this._orbit(e,s)}}_wheel(t){if(!this.enabled||!this.zoomEnabled)return;t.preventDefault();let e=t.deltaY>0?1.1:.9;if(this.zoomDeltaCallback){this.zoomDeltaCallback(e);return}this.radius=Math.max(.01,this.radius*e),this.radiusMin!==null&&(this.radius=Math.max(this.radiusMin,this.radius)),this.radiusMax!==null&&(this.radius=Math.min(this.radiusMax,this.radius))}_touchStart(t){this.enabled&&(this.allowTouchScroll&&t.touches.length<2||(t.preventDefault(),this._touches=Array.from(t.touches).map(e=>({id:e.identifier,x:e.clientX,y:e.clientY}))))}_touchMove(t){if(!this.enabled)return;if(this.allowTouchScroll&&t.touches.length<2){this._touches=[];return}t.preventDefault();let e=this._touches,s=Array.from(t.touches).map(i=>({id:i.identifier,x:i.clientX,y:i.clientY}));if(s.length===1&&e.length===1){let i=s[0].x-e[0].x,r=s[0].y-e[0].y;this.panEnabled&&(this.panButton===0||!this.orbitEnabled)?this._pan(i,r):this._orbit(i,r)}else if(s.length===2&&e.length===2){let i=Math.hypot(e[1].x-e[0].x,e[1].y-e[0].y),r=Math.hypot(s[1].x-s[0].x,s[1].y-s[0].y);if(i>0&&r>0&&this.zoomEnabled){let n=i/r;this.zoomDeltaCallback?this.zoomDeltaCallback(n):(this.radius=Math.max(.01,this.radius*n),this.radiusMin!==null&&(this.radius=Math.max(this.radiusMin,this.radius)),this.radiusMax!==null&&(this.radius=Math.min(this.radiusMax,this.radius)))}if(this.panEnabled){let n=(e[0].x+e[1].x)*.5,o=(e[0].y+e[1].y)*.5,l=(s[0].x+s[1].x)*.5,c=(s[0].y+s[1].y)*.5;this._pan(l-n,c-o)}}this._touches=s}_touchEnd(t){this._touches=Array.from(t.touches).map(e=>({id:e.identifier,x:e.clientX,y:e.clientY})),this._touches.length===0&&this.dragEndCallback?.()}_orbit(t,e){let i=-t*.005,r=e*.005;if(this.orbitDeltaCallback){this.orbitDeltaCallback(i,r);return}this.theta+=i,this.phi=Math.max(-Math.PI/2+.01,Math.min(Math.PI/2-.01,this.phi+r)),this.thetaMin!==null&&(this.theta=Math.max(this.thetaMin,this.theta)),this.thetaMax!==null&&(this.theta=Math.min(this.thetaMax,this.theta)),this.phiMin!==null&&(this.phi=Math.max(this.phiMin,this.phi)),this.phiMax!==null&&(this.phi=Math.min(this.phiMax,this.phi))}constrainAngles(t,e){if(t!==null){let s=t*Math.PI/180;this.thetaMin=this.theta-s,this.thetaMax=this.theta+s}else this.thetaMin=null,this.thetaMax=null;if(e!==null){let s=e*Math.PI/180;this.phiMin=Math.max(-Math.PI/2+.01,this.phi-s),this.phiMax=Math.min(Math.PI/2-.01,this.phi+s)}else this.phiMin=null,this.phiMax=null}clearConstraints(){this.thetaMin=null,this.thetaMax=null,this.phiMin=null,this.phiMax=null}disableZoom(){this.zoomEnabled=!1,this.radiusMin=null,this.radiusMax=null}constrainZoom(t,e){let s=e/100;this.zoomEnabled=!0,this.radiusMin=Math.max(.01,t*(1-s)),this.radiusMax=t*(1+s)}enableZoom(){this.zoomEnabled=!0,this.radiusMin=null,this.radiusMax=null}_pan(t,e){let s=this.radius*.001*this.panSpeed,i=this._cameraRight(),r=this._cameraUp(),n=-(i[0]*t-r[0]*e)*s,o=-(i[1]*t-r[1]*e)*s,l=-(i[2]*t-r[2]*e)*s;if(this.panDeltaCallback){this.panDeltaCallback(n,o,l);return}if(this.target[0]+=n,this.target[1]+=o,this.target[2]+=l,this.panRadius!==null&&this.panOrigin){let[c,u,m]=this.panOrigin,f=Math.hypot(this.target[0]-c,this.target[1]-u,this.target[2]-m);if(f>this.panRadius){let d=this.panRadius/f;this.target[0]=c+(this.target[0]-c)*d,this.target[1]=u+(this.target[1]-u)*d,this.target[2]=m+(this.target[2]-m)*d}}}_cameraRight(){return[this.viewMatrix[0],this.viewMatrix[4],this.viewMatrix[8]]}_cameraUp(){return[this.viewMatrix[1],this.viewMatrix[5],this.viewMatrix[9]]}update(t,e){let s=this._eye();gs(s,this.target,[0,1,0],this.viewMatrix),ys(this.fov,t/e,this.near,this.far,this.projMatrix)}get eye(){return this._eye()}_eye(){let t=Math.cos(this.phi),e=Math.sin(this.phi),s=Math.cos(this.theta),i=Math.sin(this.theta);return[this.target[0]+this.radius*t*i,this.target[1]+this.radius*e,this.target[2]+this.radius*t*s]}focalLength(t){return t*.5/Math.tan(this.fov*.5)}setFromLookAt(t,e){this.target=[e[0],e[1],e[2]];let s=t[0]-e[0],i=t[1]-e[1],r=t[2]-e[2];this.radius=Math.hypot(s,i,r)||.001,this.phi=Math.asin(Math.max(-1,Math.min(1,i/this.radius))),this.theta=Math.atan2(s,r)}fitScene(t,e){this._sceneBounds(t,e),this.theta=0,this.phi=.2}focusScene(t,e){this._sceneBounds(t,e)}_sceneBounds(t,e){let s=1/0,i=1/0,r=1/0,n=-1/0,o=-1/0,l=-1/0;for(let u=0;u<e;u++){let m=u*16,f=t[m],d=t[m+1],h=t[m+2];f<s&&(s=f),f>n&&(n=f),d<i&&(i=d),d>o&&(o=d),h<r&&(r=h),h>l&&(l=h)}this.target=[(s+n)*.5,(i+o)*.5,(r+l)*.5];let c=Math.max(n-s,o-i,l-r)*.5;this.radius=c/Math.tan(this.fov*.5)*1.2}};function gs(a,t,e,s){let[i,r,n]=a,[o,l,c]=t,[u,m,f]=e,d=i-o,h=r-l,p=n-c,w=Math.hypot(d,h,p);d/=w,h/=w,p/=w;let y=m*p-f*h,g=f*d-u*p,S=u*h-m*d,_=Math.hypot(y,g,S);y/=_,g/=_,S/=_;let v=h*S-p*g,B=p*y-d*S,b=d*g-h*y;s[0]=y,s[1]=v,s[2]=d,s[3]=0,s[4]=g,s[5]=B,s[6]=h,s[7]=0,s[8]=S,s[9]=b,s[10]=p,s[11]=0,s[12]=-(y*i+g*r+S*n),s[13]=-(v*i+B*r+b*n),s[14]=-(d*i+h*r+p*n),s[15]=1}function ys(a,t,e,s,i){let r=1/Math.tan(a*.5),n=e-s;i[0]=r/t,i[1]=0,i[2]=0,i[3]=0,i[4]=0,i[5]=r,i[6]=0,i[7]=0,i[8]=0,i[9]=0,i[10]=s/n,i[11]=-1,i[12]=0,i[13]=0,i[14]=e*s/n,i[15]=0}var vs=`
|
|
649
|
+
function radixSort32(keys, idx, tmp_k, tmp_i, counts, pfx, N) {
|
|
650
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
651
|
+
const shift = pass * 8;
|
|
652
|
+
counts.fill(0);
|
|
653
|
+
for (let i = 0; i < N; i++) counts[(keys[i] >>> shift) & 0xff]++;
|
|
654
|
+
pfx[0] = 0;
|
|
655
|
+
for (let i = 1; i < 256; i++) pfx[i] = pfx[i-1] + counts[i-1];
|
|
656
|
+
for (let i = 0; i < N; i++) {
|
|
657
|
+
const b = (keys[i] >>> shift) & 0xff;
|
|
658
|
+
tmp_k[pfx[b]] = keys[i];
|
|
659
|
+
tmp_i[pfx[b]++] = idx[i];
|
|
660
|
+
}
|
|
661
|
+
// swap
|
|
662
|
+
const k = keys; keys = tmp_k; tmp_k = k;
|
|
663
|
+
const x = idx; idx = tmp_i; tmp_i = x;
|
|
664
|
+
}
|
|
665
|
+
// after 4 passes (even) idx holds the result
|
|
666
|
+
return idx;
|
|
667
|
+
}
|
|
668
|
+
`,bs=vs+`
|
|
669
|
+
const u32 = new Uint32Array(1);
|
|
670
|
+
const f32 = new Float32Array(u32.buffer);
|
|
671
|
+
|
|
672
|
+
let keys0, keys1, idx0, idx1, counts, pfx, allocN = -1;
|
|
673
|
+
function alloc(N) {
|
|
674
|
+
if (N <= allocN) return;
|
|
675
|
+
allocN = N;
|
|
676
|
+
keys0 = new Uint32Array(N);
|
|
677
|
+
keys1 = new Uint32Array(N);
|
|
678
|
+
idx0 = new Uint32Array(N);
|
|
679
|
+
idx1 = new Uint32Array(N);
|
|
680
|
+
counts = new Uint32Array(256);
|
|
681
|
+
pfx = new Uint32Array(256);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
self.onmessage = function(e) {
|
|
685
|
+
const depths = e.data.depths;
|
|
686
|
+
const gIndex = e.data.gIndex;
|
|
687
|
+
const N = e.data.N;
|
|
688
|
+
alloc(N);
|
|
689
|
+
|
|
690
|
+
// Convert negative floats \u2192 sortable uint32 by flipping all bits
|
|
691
|
+
const dv = new DataView(depths.buffer);
|
|
692
|
+
for (let i = 0; i < N; i++) {
|
|
693
|
+
keys0[i] = dv.getUint32(i * 4, true) ^ 0xffffffff;
|
|
694
|
+
idx0[i] = gIndex ? gIndex[i] : i;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const sorted = radixSort32(keys0, idx0, keys1, idx1, counts, pfx, N);
|
|
698
|
+
const result = sorted.slice(0, N);
|
|
699
|
+
self.postMessage({ order: result }, [result.buffer]);
|
|
700
|
+
};
|
|
701
|
+
`;function Ee(a){let t=new Uint32Array(a),e=new Uint32Array(a),s=new Uint32Array(a),i=new Uint32Array(a),r=new Uint32Array(256),n=new Uint32Array(256);function o(l,c,u=null){let m=new DataView(l.buffer);for(let f=0;f<c;f++){let d=u?u[f]:f;t[f]=m.getUint32(d*4,!0)^4294967295,s[f]=d}for(let f=0;f<4;f++){let d=f*8;r.fill(0);for(let h=0;h<c;h++)r[t[h]>>>d&255]++;n[0]=0;for(let h=1;h<256;h++)n[h]=n[h-1]+r[h-1];for(let h=0;h<c;h++){let p=t[h]>>>d&255;e[n[p]]=t[h],i[n[p]++]=s[h]}[t,e]=[e,t],[s,i]=[i,s]}return s}return o}function _t(a){if(typeof Worker>"u")return Ee(a);let t;try{let r=new Blob([bs],{type:"application/javascript"}),n=URL.createObjectURL(r);t=new Worker(n),URL.revokeObjectURL(n)}catch{return Ee(a)}let e=new Uint32Array(a);for(let r=0;r<a;r++)e[r]=r;let s=!1,i=null;return t.onmessage=r=>{if(e=r.data.order,s=!1,i){let{depths:n,N:o,gIndex:l}=i;i=null,s=!0;let c=l?[n.buffer,l.buffer]:[n.buffer];t.postMessage({depths:n,N:o,gIndex:l},c)}},function(n,o,l=null){let c=new Float32Array(o);if(l)for(let m=0;m<o;m++)c[m]=n[l[m]];else c.set(n.subarray(0,o));let u=l?l.slice(0,o):null;if(s)i={depths:c,N:o,gIndex:u};else{s=!0;let m=u?[c.buffer,u.buffer]:[c.buffer];t.postMessage({depths:c,N:o,gIndex:u},m)}return e.length!==o?u&&u.length===o?u:Uint32Array.from({length:o},(m,f)=>f):e}}async function at(a,t){let e=await fetch(a);if(!e.ok)throw new Error(`HTTP ${e.status} loading ${a}`);let s=parseInt(e.headers.get("content-length")||"0",10),i=e.body.getReader(),r=[],n=0;for(;;){let{done:c,value:u}=await i.read();if(c)break;r.push(u),n+=u.byteLength,t&&s>0&&t(n/s)}let o=new Uint8Array(n),l=0;for(let c of r)o.set(c,l),l+=c.byteLength;return o.buffer}async function ze(a,t){let e=await at(a,t);return Te(e)}function Te(a){let e=Math.floor(a.byteLength/32);if(e===0)throw new Error("Empty or invalid .splat file");let s=new DataView(a),i=new Float32Array(e*16);for(let r=0;r<e;r++){let n=r*32,o=r*16;i[o+0]=s.getFloat32(n+0,!0),i[o+1]=s.getFloat32(n+4,!0),i[o+2]=s.getFloat32(n+8,!0),i[o+4]=s.getUint8(n+24)/255,i[o+5]=s.getUint8(n+25)/255,i[o+6]=s.getUint8(n+26)/255,i[o+7]=s.getUint8(n+27)/255,i[o+8]=s.getFloat32(n+12,!0),i[o+9]=s.getFloat32(n+16,!0),i[o+10]=s.getFloat32(n+20,!0);let l=(s.getUint8(n+28)-128)/128,c=(s.getUint8(n+29)-128)/128,u=(s.getUint8(n+30)-128)/128,m=(s.getUint8(n+31)-128)/128,f=Math.hypot(c,u,m,l)||1;i[o+12]=c/f,i[o+13]=u/f,i[o+14]=m/f,i[o+15]=l/f}return{data:i,count:e,shData:null,numSHBases:0}}var ee=.28209479177387814;async function Ue(a,t,e=0){let s=await at(a,t);return De(s,e)}function De(a,t=0){let e=new Uint8Array(a),s=Ge(e);if(s<0)throw new Error("Invalid PLY: end_header not found");let{numVertices:i,propMap:r,stride:n,hasColor:o}=Oe(new TextDecoder().decode(e.subarray(0,s)));if(i===0)throw new Error("PLY file contains no vertices");let l=new DataView(a,s),{data:c,shData:u,numSHBases:m}=Re(l,r,n,o,i,t);return{data:c,count:i,shData:u,numSHBases:m}}async function se(a,t,e=0){let s=await fetch(a);if(!s.ok)throw new Error(`HTTP ${s.status} loading ${a}`);let i=parseInt(s.headers.get("content-length")||"0",10),r=s.body.getReader(),n=new Uint8Array(0),o=0;for(;;){let{done:l,value:c}=await r.read();if(l)throw new Error("PLY stream ended before end_header");o+=c.byteLength,t&&i>0&&t(o/i);let u=new Uint8Array(n.length+c.length);u.set(n),u.set(c,n.length),n=u;let m=Ge(n);if(m>=0){let f=Oe(new TextDecoder().decode(n.subarray(0,m))),d=n.slice(m),{numVertices:h,propMap:p,stride:w,hasColor:y}=f,g=p.f_rest_44?15:p.f_rest_23?8:p.f_rest_8?3:0,S=g>=15?3:g>=8?2:g>=3?1:0,_=Math.min(e,S),v=_>=3?15:_>=2?8:_>=1?3:0;return{numVertices:h,numSHBases:v,consume:async(b,P)=>{let M=d;for(;;){let{done:U,value:I}=await r.read();if(!U){o+=I.byteLength,P&&i>0&&P(o/i);let z=new Uint8Array(M.length+I.length);z.set(M),z.set(I,M.length),M=z}let V=Math.floor(M.length/w);if(V>0){let z=V*w,{data:T,shData:F}=Re(new DataView(M.buffer,M.byteOffset,z),p,w,y,V,e);b(T,V,F),M=M.slice(z)}if(U)break}}}}if(n.length>65536)throw new Error("PLY header exceeds 64 KB")}}function Re(a,t,e,s,i,r=0){let n=new Float32Array(i*16),o=t.f_rest_44?15:t.f_rest_23?8:t.f_rest_8?3:0,l=o>=15?3:o>=8?2:o>=3?1:0,c=Math.min(r,l),u=c>=3?15:c>=2?8:c>=1?3:0,m=u>0&&s,f=[],d=[],h=[];if(m){let w=o,y=2*o;for(let g=0;g<u;g++)f[g]=t[`f_rest_${g}`],d[g]=t[`f_rest_${w+g}`],h[g]=t[`f_rest_${y+g}`]}let p=m?new Float32Array(i*u*3):null;for(let w=0;w<i;w++){let y=w*e,g=w*16;if(n[g+0]=q(a,y,t.x),n[g+1]=q(a,y,t.y),n[g+2]=q(a,y,t.z),s){if(n[g+4]=.5+ee*q(a,y,t.f_dc_0),n[g+5]=.5+ee*q(a,y,t.f_dc_1),n[g+6]=.5+ee*q(a,y,t.f_dc_2),m){let P=w*u*3;for(let M=0;M<u;M++){let U=P+M*3;p[U+0]=q(a,y,f[M]),p[U+1]=q(a,y,d[M]),p[U+2]=q(a,y,h[M])}}}else n[g+4]=1,n[g+5]=1,n[g+6]=1;n[g+7]=ws(q(a,y,t.opacity)),n[g+8]=Math.exp(q(a,y,t.scale_0)),n[g+9]=Math.exp(q(a,y,t.scale_1)),n[g+10]=Math.exp(q(a,y,t.scale_2));let S=q(a,y,t.rot_0),_=q(a,y,t.rot_1),v=q(a,y,t.rot_2),B=q(a,y,t.rot_3),b=Math.hypot(_,v,B,S)||1;n[g+12]=_/b,n[g+13]=v/b,n[g+14]=B/b,n[g+15]=S/b}return{data:n,shData:p,numSHBases:u}}function Ge(a){let t=[101,110,100,95,104,101,97,100,101,114];t:for(let e=0;e<=a.length-t.length;e++){for(let i=0;i<t.length;i++)if(a[e+i]!==t[i])continue t;let s=e+t.length;return a[s]===13&&s++,a[s]===10&&s++,s}return-1}function Oe(a){let t=a.split(`
|
|
702
|
+
`),e=0,s=!1,i=[];for(let l of t){let c=l.trim().split(/\s+/);c[0]==="element"?(s=c[1]==="vertex",s&&(e=parseInt(c[2],10))):c[0]==="property"&&s&&i.push({type:c[1],name:c[2]})}let r={},n=0;for(let l of i)r[l.name]={offset:n,type:l.type},n+=xs(l.type);let o=["x","y","z","scale_0","scale_1","scale_2","rot_0","rot_1","rot_2","rot_3","opacity"];for(let l of o)if(!r[l])throw new Error(`PLY missing required property: ${l}`);return{numVertices:e,propMap:r,stride:n,hasColor:!!(r.f_dc_0&&r.f_dc_1&&r.f_dc_2)}}function xs(a){switch(a){case"float":case"float32":case"int":case"uint":return 4;case"double":case"int64":case"uint64":return 8;case"short":case"ushort":case"int16":case"uint16":return 2;case"char":case"uchar":case"int8":case"uint8":return 1;default:return 4}}function q(a,t,e){let s=t+e.offset;switch(e.type){case"float":case"float32":return a.getFloat32(s,!0);case"double":return a.getFloat64(s,!0);case"int":case"int32":return a.getInt32(s,!0);case"uint":case"uint32":return a.getUint32(s,!0);case"short":case"int16":return a.getInt16(s,!0);case"ushort":case"uint16":return a.getUint16(s,!0);case"char":case"int8":return a.getInt8(s);case"uchar":case"uint8":return a.getUint8(s);default:return a.getFloat32(s,!0)}}function ws(a){return 1/(1+Math.exp(-a))}var He=1347635022,Ie=1/Math.SQRT2;async function Ve(a,t,e=0){let s=await at(a,t),i=await Mt(s);return ie(i,e)}async function Ss(a,t=0){let e=await Mt(a);return ie(e,t)}var Ps=5e4;function Bs(){return new Promise(a=>setTimeout(a,0))}async function ie(a,t=0){let e=new DataView(a),s=e.getUint32(0,!0),i=e.getUint32(4,!0),r=e.getUint32(8,!0),n=e.getUint8(12),o=e.getUint8(13);if(s!==He)throw new Error(`Invalid .spz magic: 0x${s.toString(16).toUpperCase()} (expected 0x${He.toString(16).toUpperCase()})`);(i<2||i>4)&&console.warn(`HoloSplat: .spz version ${i} is untested; attempting load anyway`);let l=1<<o,c=i>=3?4:3,u=b=>b>=4?24:b>=3?15:b>=2?8:b>=1?3:0,m=u(n),f=Math.min(t,n,3),d=u(f),h=d>0,p=16,w=p+r*9,y=w+r*1,g=y+r*3,S=g+r*3,_=S+r*c,v=new Float32Array(r*16),B=h?new Float32Array(r*d*3):null;for(let b=0;b<r;b++){let P=b*16,M=p+b*9;v[P+0]=ct(e,M+0)/l,v[P+1]=ct(e,M+3)/l,v[P+2]=ct(e,M+6)/l;let U=y+b*3;v[P+4]=e.getUint8(U+0)/255,v[P+5]=e.getUint8(U+1)/255,v[P+6]=e.getUint8(U+2)/255,v[P+7]=e.getUint8(w+b)/255;let I=g+b*3;v[P+8]=Math.exp((e.getUint8(I+0)-128)/16),v[P+9]=Math.exp((e.getUint8(I+1)-128)/16),v[P+10]=Math.exp((e.getUint8(I+2)-128)/16);let V=S+b*c,z,T,F,Y;if(i>=3){let j=e.getUint32(V,!0),xt=j&3,W=Ie/512,$=gt(j>>2&1023)*W,L=gt(j>>12&1023)*W,Q=gt(j>>22&1023)*W,ot=Math.sqrt(Math.max(0,1-$*$-L*L-Q*Q));switch(xt){case 0:z=ot,T=$,F=L,Y=Q;break;case 1:z=$,T=ot,F=L,Y=Q;break;case 2:z=$,T=L,F=ot,Y=Q;break;default:z=$,T=L,F=Q,Y=ot}}else z=e.getInt8(V+0)/128,T=e.getInt8(V+1)/128,F=e.getInt8(V+2)/128,Y=Math.sqrt(Math.max(0,1-z*z-T*T-F*F));let rt=Math.hypot(z,T,F,Y)||1;if(v[P+12]=z/rt,v[P+13]=T/rt,v[P+14]=F/rt,v[P+15]=Y/rt,h){let j=_+b*m*3,xt=b*d*3;for(let W=0;W<d*3;W++)B[xt+W]=ks(e.getUint8(j+W))}(b+1)%Ps===0&&await Bs()}return{data:v,count:r,shData:B,numSHBases:d}}function ct(a,t){let e=a.getUint8(t),s=a.getUint8(t+1),i=a.getInt8(t+2);return e|s<<8|i<<16}function gt(a){return a&512?a|4294966272:a}function Gt(a){return Math.exp((a-128)/16)}function ks(a){return(a-128)/128}function Le(a){let t=a&3,e=Ie/512,s=gt(a>>2&1023)*e,i=gt(a>>12&1023)*e,r=gt(a>>22&1023)*e,n=Math.sqrt(Math.max(0,1-s*s-i*i-r*r)),o,l,c,u;switch(t){case 0:o=n,l=s,c=i,u=r;break;case 1:o=s,l=n,c=i,u=r;break;case 2:o=s,l=i,c=n,u=r;break;default:o=s,l=i,c=r,u=n}let m=Math.hypot(o,l,c,u)||1;return[o/m,l/m,c/m,u/m]}async function Mt(a){if(typeof DecompressionStream>"u")throw new Error("DecompressionStream is not available in this environment");let t=new DecompressionStream("gzip"),e=t.writable.getWriter();return e.write(a),e.close(),new Response(t.readable).arrayBuffer()}async function Ms(a,t,e={}){let s=qe(a,t,e);return ne(s)}function qe(a,t,e={}){let{fractionalBits:s}=e;if(s==null){let _=0;for(let B=0;B<t;B++){let b=B*16;Math.abs(a[b])>_&&(_=Math.abs(a[b])),Math.abs(a[b+1])>_&&(_=Math.abs(a[b+1])),Math.abs(a[b+2])>_&&(_=Math.abs(a[b+2]))}let v=(1<<23)-1;s=_>0?Math.min(20,Math.max(0,Math.floor(Math.log2(v/_)))):12}let{shData:i,numSHBases:r=0}=e,n=r>=15?3:r>=8?2:r>=3?1:0,o=r*3,l=16,c=l+t*(20+o),u=new ArrayBuffer(c),m=new DataView(u),f=new Uint8Array(u);m.setUint32(0,1347635022,!0),m.setUint32(4,3,!0),m.setUint32(8,t,!0),m.setUint8(12,n),m.setUint8(13,s),m.setUint8(14,0),m.setUint8(15,0);let d=l,h=d+t*9,p=h+t*1,w=p+t*3,y=w+t*3,g=y+t*4,S=1<<s;for(let _=0;_<t;_++){let v=_*16;if(ht(m,d+_*9+0,a[v+0]*S),ht(m,d+_*9+3,a[v+1]*S),ht(m,d+_*9+6,a[v+2]*S),f[h+_]=X(a[v+7]*255),f[p+_*3+0]=X(a[v+4]*255),f[p+_*3+1]=X(a[v+5]*255),f[p+_*3+2]=X(a[v+6]*255),f[w+_*3+0]=ut(a[v+8]),f[w+_*3+1]=ut(a[v+9]),f[w+_*3+2]=ut(a[v+10]),m.setUint32(y+_*4,ae(a[v+12],a[v+13],a[v+14],a[v+15]),!0),i&&r>0){let B=_*o,b=_*r*3;for(let P=0;P<o;P++)f[g+B+P]=Cs(i[b+P])}}return f}function ht(a,t,e){let s=Math.max(-8388608,Math.min(8388607,Math.round(e)));a.setUint8(t,s&255),a.setUint8(t+1,s>>8&255),a.setUint8(t+2,s>>16&255)}function X(a){return Math.max(0,Math.min(255,Math.round(a)))}function ut(a){return X(Math.log(Math.max(1e-9,a))*16+128)}function Cs(a){return X(Math.round(a*128)+128)}function ae(a,t,e,s){let i=Math.hypot(a,t,e,s)||1,r=[a/i,t/i,e/i,s/i],n=0;for(let h=1;h<4;h++)Math.abs(r[h])>Math.abs(r[n])&&(n=h);let o=r[n]<0?-1:1,l=[0,1,2,3].filter(h=>h!==n),c=512*Math.SQRT2,u=h=>Math.max(-512,Math.min(511,Math.round(r[h]*o*c))),m=u(l[0]),f=u(l[1]),d=u(l[2]);return(n&3|(m&1023)<<2|(f&1023)<<12|(d&1023)<<22)>>>0}async function ne(a){if(typeof CompressionStream>"u")throw new Error("CompressionStream API is not available in this environment");let t=new CompressionStream("gzip"),e=t.writable.getWriter();return e.write(a),e.close(),new Response(t.readable).arrayBuffer()}var Ot=1448104776,As=1,ft=16;function je(a,t,e={}){if(!a.length)throw new Error("HoloSplat: encodeSpzv requires at least one variant");if(a.length>255)throw new Error("HoloSplat: encodeSpzv supports at most 255 variants");let s=a[0].data,{fractionalBits:i}=e;if(i==null){let _=0;for(let B=0;B<t;B++){let b=B*16;_=Math.max(_,Math.abs(s[b]),Math.abs(s[b+1]),Math.abs(s[b+2]))}let v=(1<<23)-1;i=_>0?Math.min(20,Math.max(0,Math.floor(Math.log2(v/_)))):12}let r=a.length,n=16,o=r*ft,l=t*16,c=t*4,u=n+o+l+r*c,m=new ArrayBuffer(u),f=new DataView(m),d=new Uint8Array(m);f.setUint32(0,Ot,!0),f.setUint32(4,As,!0),f.setUint32(8,t,!0),f.setUint8(12,r),f.setUint8(13,i),f.setUint8(14,0),f.setUint8(15,0);let h=n,p=new TextEncoder;for(let _ of a){let v=p.encode(_.name).slice(0,ft-1);d.set(v,h),h+=ft}let w=h,y=w+t*9,g=y+t*3;h=g+t*4;let S=1<<i;for(let _=0;_<t;_++){let v=_*16;ht(f,w+_*9+0,s[v+0]*S),ht(f,w+_*9+3,s[v+1]*S),ht(f,w+_*9+6,s[v+2]*S),d[y+_*3+0]=ut(s[v+8]),d[y+_*3+1]=ut(s[v+9]),d[y+_*3+2]=ut(s[v+10]),f.setUint32(g+_*4,ae(s[v+12],s[v+13],s[v+14],s[v+15]),!0)}for(let _ of a){let v=h,B=v+t;for(let b=0;b<t;b++){let P=b*16;d[v+b]=X(_.data[P+7]*255),d[B+b*3+0]=X(_.data[P+4]*255),d[B+b*3+1]=X(_.data[P+5]*255),d[B+b*3+2]=X(_.data[P+6]*255)}h=B+t*3}return d}async function Fs(a,t,e={}){let s=je(a,t,e);return ne(s)}var Es=5e4;function Ne(){return new Promise(a=>setTimeout(a,0))}async function $e(a,t){let e=await at(a,t),s=await Mt(e);return re(s)}async function zs(a){let t=await Mt(a);return re(t)}async function re(a){let t=new DataView(a),e=new Uint8Array(a),s=t.getUint32(0,!0),i=t.getUint32(4,!0),r=t.getUint32(8,!0),n=t.getUint8(12),o=t.getUint8(13);if(s!==Ot)throw new Error(`Invalid .spzv magic: 0x${s.toString(16).toUpperCase()} (expected 0x${Ot.toString(16).toUpperCase()})`);i!==1&&console.warn(`HoloSplat: .spzv version ${i} is untested; attempting load anyway`);let l=16,c=new TextDecoder,u=[];for(let y=0;y<n;y++){let g=e.subarray(l,l+ft),S=g.indexOf(0);u.push(c.decode(g.subarray(0,S<0?ft:S))),l+=ft}let m=l,f=m+r*9,d=f+r*3;l=d+r*4;let h=1<<o,p=new Float32Array(r*16);for(let y=0;y<r;y++){let g=y*16,S=m+y*9;p[g+0]=ct(t,S+0)/h,p[g+1]=ct(t,S+3)/h,p[g+2]=ct(t,S+6)/h;let _=f+y*3;p[g+8]=Gt(e[_+0]),p[g+9]=Gt(e[_+1]),p[g+10]=Gt(e[_+2]);let[v,B,b,P]=Le(t.getUint32(d+y*4,!0));p[g+12]=v,p[g+13]=B,p[g+14]=b,p[g+15]=P,(y+1)%Es===0&&await Ne()}let w=[];for(let y=0;y<n;y++){let g=l,S=g+r,_=new Float32Array(r*4);for(let v=0;v<r;v++){let B=v*4;if(_[B+0]=e[S+v*3+0]/255,_[B+1]=e[S+v*3+1]/255,_[B+2]=e[S+v*3+2]/255,_[B+3]=e[g+v]/255,y===0){let b=v*16;p[b+4]=_[B+0],p[b+5]=_[B+1],p[b+6]=_[B+2],p[b+7]=_[B+3]}}l=S+r*3,w.push({name:u[y],palette:_}),(y+1)%1===0&&await Ne()}return{data:p,count:r,variants:w,shData:null,numSHBases:0}}function yt(a,t,e,s,i,r,n,o,l){let c=a*i+t*r+e*n+s*o;if(c<0&&(i=-i,r=-r,n=-n,o=-o,c=-c),c>.9995){let w=a+(i-a)*l,y=t+(r-t)*l,g=e+(n-e)*l,S=s+(o-s)*l,_=Math.sqrt(w*w+y*y+g*g+S*S);return[w/_,y/_,g/_,S/_]}let u=Math.acos(c),m=u*l,f=Math.sin(u),d=Math.sin(m),h=Math.cos(m)-c*d/f,p=d/f;return[h*a+p*i,h*t+p*r,h*e+p*n,h*s+p*o]}function oe(a){let t=a.replace(/^hs-part\./,"").replace(/^ctrl\./,"");return t=t.replace(/(\.\d+)+$/,""),t}var Ht=class{constructor(t){if(!t.frames||t.frames.length===0)throw new Error("HoloSplat Animation: no frame data");this.fps=t.fps??24,this.frameCount=t.frameCount??Math.floor(t.frames.length/6),this.fov=t.fov??null,this.near=t.near??null,this.far=t.far??null,this.callouts=t.callouts??[],this.focalPoint=t.focalPoint??null,this._focalFrames=t.focalFrames?new Float32Array(t.focalFrames):null,this.loop=!0,Array.isArray(t.markers)?this.markers=Object.fromEntries(t.markers.map(e=>[e.name,e.frame])):this.markers=t.markers??{},this.stateCalls=t.stateCalls??[],this._frames=new Float32Array(t.frames),this._objects=(t.objects??[]).map(e=>({id:e.id,frames:new Float32Array(e.frames),variants:e.variants})),this._volumes=(t.volumes??[]).map(e=>({name:e.name,softEdge:e.softEdge??.05,matrices:new Float32Array(e.matrices)})),this._anchors=(t.anchors??[]).map(e=>({asset:e.asset,frames:new Float32Array(e.frames)})),this._time=0,this._playing=!0,this.direction=1,this.pingPong=!1}get duration(){return this.frameCount/this.fps}get time(){return this._time}get playing(){return this._playing}get objects(){return this._objects}get volumes(){return this._volumes}get anchors(){return this._anchors}play(){this._playing=!0}pause(){this._playing=!1}seek(t){this._time=Math.max(0,Math.min(this.duration,t))}seekFrame(t){this._time=Math.max(0,Math.min(this.frameCount-1,t))/this.fps}get frame(){return this._time*this.fps}tick(t){this._playing&&(this._time+=t*this.direction,this.direction>=0?this._time>=this.duration&&(this.pingPong?(this.direction=-1,this._time=2*this.duration-this._time):(this._time=this.loop?this._time%this.duration:this.duration,this.loop||(this._playing=!1))):this._time<=0&&(this.pingPong?(this.direction=1,this._time=-this._time):this.loop?this._time=this.duration+this._time:(this._time=0,this._playing=!1)))}getObjectFrames(t){let e=t!=null?Math.max(0,Math.min(t,this.frameCount-1)):Math.min(this._time*this.fps,this.frameCount-1),s=Math.min(Math.floor(e),this.frameCount-1),i=Math.min(s+1,this.frameCount-1),r=e-s;return this._objects.map(n=>{let o=n.frames,l=s*7,c=i*7;return{id:n.id,pos:[o[l]+(o[c]-o[l])*r,o[l+1]+(o[c+1]-o[l+1])*r,o[l+2]+(o[c+2]-o[l+2])*r],quat:yt(o[l+3],o[l+4],o[l+5],o[l+6],o[c+3],o[c+4],o[c+5],o[c+6],r)}})}getAnchorFrames(t){let e=t!=null?Math.max(0,Math.min(t,this.frameCount-1)):Math.min(this._time*this.fps,this.frameCount-1),s=Math.min(Math.floor(e),this.frameCount-1),i=Math.min(s+1,this.frameCount-1),r=e-s;return this._anchors.map(n=>{let o=n.frames,l=s*7,c=i*7;return{asset:n.asset,pos:[o[l]+(o[c]-o[l])*r,o[l+1]+(o[c+1]-o[l+1])*r,o[l+2]+(o[c+2]-o[l+2])*r],quat:yt(o[l+3],o[l+4],o[l+5],o[l+6],o[c+3],o[c+4],o[c+5],o[c+6],r)}})}getVolumeFrames(t){let e=t!=null?Math.max(0,Math.min(t,this.frameCount-1)):Math.min(this._time*this.fps,this.frameCount-1),s=Math.min(Math.floor(e),this.frameCount-1),i=Math.min(s+1,this.frameCount-1),r=e-s;return this._volumes.map(n=>{let o=n.matrices,l=s*16,c=i*16,u=new Float32Array(16);for(let m=0;m<16;m++)u[m]=o[l+m]+(o[c+m]-o[l+m])*r;return{name:n.name,softEdge:n.softEdge,matrix:u}})}getFocalPoint(){if(!this.focalPoint)return null;if(!this._focalFrames)return this.focalPoint;let t=Math.min(this._time*this.fps,this.frameCount-1),e=Math.min(Math.floor(t),this.frameCount-1),s=Math.min(e+1,this.frameCount-1),i=t-e,r=this._focalFrames,n=e*3,o=s*3;return[r[n]+(r[o]-r[n])*i,r[n+1]+(r[o+1]-r[n+1])*i,r[n+2]+(r[o+2]-r[n+2])*i]}getCameraFrame(t){let e=t!=null?Math.max(0,Math.min(t,this.frameCount-1)):Math.min(this._time*this.fps,this.frameCount-1),s=Math.min(Math.floor(e),this.frameCount-1),i=Math.min(s+1,this.frameCount-1),r=e-s,n=this._frames,o=s*6,l=i*6,c=n[o]+(n[l]-n[o])*r,u=n[o+1]+(n[l+1]-n[o+1])*r,m=n[o+2]+(n[l+2]-n[o+2])*r,f=n[o+3]+(n[l+3]-n[o+3])*r,d=n[o+4]+(n[l+4]-n[o+4])*r,h=n[o+5]+(n[l+5]-n[o+5])*r;return{eye:[c,u,m],target:[c+f,u+d,m+h]}}};function Ye(a,t){let e=Math.max(0,Math.min(Math.floor(t),a.frameCount-1)),s=Math.min(e+1,a.frameCount-1),i=Math.max(0,Math.min(1,t-e));return a.objects.map(r=>{let n=r.frames,o=e*7,l=s*7;return{id:r.id,pos:[n[o]+(n[l]-n[o])*i,n[o+1]+(n[l+1]-n[o+1])*i,n[o+2]+(n[l+2]-n[o+2])*i],quat:yt(n[o+3],n[o+4],n[o+5],n[o+6],n[l+3],n[l+4],n[l+5],n[l+6],i)}})}function We(a,t){let e=a.masks;if(!e||!e.length)return[];let s=Math.max(0,Math.min(Math.floor(t),a.frameCount-1)),i=Math.min(s+1,a.frameCount-1),r=Math.max(0,Math.min(1,t-s));return e.map(({name:n,softEdge:o,matrices:l})=>{let c=s*16,u=i*16,m=new Float32Array(16);for(let f=0;f<16;f++)m[f]=l[c+f]+(l[u+f]-l[c+f])*r;return{name:n,softEdge:o,matrix:m}})}function le(a,t,e){let s=Math.max(0,Math.min(Math.floor(e),t-1)),i=Math.min(s+1,t-1),r=Math.max(0,Math.min(1,e-s)),n=s*7,o=i*7;return{pos:[a[n]+(a[o]-a[n])*r,a[n+1]+(a[o+1]-a[n+1])*r,a[n+2]+(a[o+2]-a[n+2])*r],quat:yt(a[n+3],a[n+4],a[n+5],a[n+6],a[o+3],a[o+4],a[o+5],a[o+6],r)}}function ce(a,t,e){let s=Math.max(0,Math.min(Math.floor(e),t-1)),i=Math.min(s+1,t-1),r=Math.max(0,Math.min(1,e-s)),n=s*16,o=i*16,l=new Float32Array(16);for(let c=0;c<16;c++)l[c]=a[n+c]+(a[o+c]-a[n+c])*r;return l}async function he(a){let t=await fetch(a);if(!t.ok)throw new Error(`HoloSplat: failed to load animation "${a}" (HTTP ${t.status})`);return new Ht(await t.json())}var Qe={low:{maxPixelRatio:1,shDegreeCap:1,lod:3,prefetchVariants:!1},medium:{maxPixelRatio:1.5,shDegreeCap:1,lod:2,prefetchVariants:!1},high:{maxPixelRatio:2,shDegreeCap:1/0,lod:0,prefetchVariants:!0}};function It(){if(typeof navigator>"u")return"high";if(navigator.connection?.saveData)return"low";let a=navigator.deviceMemory,t=navigator.hardwareConcurrency||4,e=/Android|iPhone|iPad|iPod/i.test(navigator.userAgent||"");return a!=null&&a<=2||t<=2||e?"low":a!=null&&a<=4||t<=4?"medium":"high"}function Vt(a){return Qe[a]||Qe.high}async function et(a,t){if(!t)return a;let e=a.match(/\.(spz|ply|splat)$/i);if(!e)return a;let s=a.slice(0,-e[0].length),i=s.lastIndexOf("/"),r=s.slice(0,i+1),n=s.slice(i+1),o=`${r}${n}.lods/${n}.lod${t}.spz`;try{if((await fetch(o,{method:"HEAD",cache:"no-store"})).ok)return o}catch{}return a}async function dt(a,t){if(!t)return a;let e={};for(let[s,i]of Object.entries(a))Array.isArray(i)?e[s]=await Promise.all(i.map(r=>et(r,t))):i&&typeof i=="object"?e[s]={...i,url:await et(i.url,t)}:e[s]=await et(i,t);return e}var bt=class{constructor(t={}){let{canvas:e,background:s="#000000",fov:i=60,near:r=.01,far:n=2e3,splatScale:o=1.08,autoRotate:l=!1,flipY:c=!1,shDegree:u=0,aaDilation:m=.3,maxPixelRatio:f=2,adaptiveQuality:d=!0,prefetchVariants:h=!0,gpuSort:p=!1,tier:w=null,onProgress:y,onError:g}=t;this._canvas=qs(e),this._onProgress=y,this._onError=g,this._autoRotate=l,this._splatScale=o,this._maxPixelRatio=f,this._flipY=c,this._shDegree=u,this._aaDilation=m,this._tier=w,this._adaptiveQuality=d,this._prefetchVariantsEnabled=h,this._gpuSort=p,this._effectivePixelRatio=f,this._minPixelRatio=.5,this._frameTimeEMA=null,this._lastQualityCheck=0,this._wasSceneReady=!1,this._qualityWarmupUntil=0,this._effectiveRadiusCap=1,this._minRadiusCap=.4,this._maxRadiusCap=1,this._renderer=new Dt(this._canvas,s),this._camera=new Rt({fov:i,near:r,far:n}),this._gaussians=null,this._numSplats=0,this._depths=null,this._sort=null,this._rafId=null,this._running=!1,this._resizeObs=null,this._resizeTimer=null,this._partIndex={},this._fileNames=[],this._partVariants={},this._partTransforms=[nt.slice()],this._partTransFlat=nt.slice(),this._partLocalPose={},this._fileRanges=null,this._fileAABB=null,this._fileMasks=null,this._activeIdx=null,this._lastActiveCount=0,this._currentSceneName=null,this._animation=null,this._animPaused=!1,this._cameraFree=!1,this._blendBack=null,this._sceneBlend=null,this._sceneReady=!1,this._lastUrl=null,this._lastParts=null,this._lastTick=null,this.onFrame=null,this._lastSortView=null,this._sortDirty=!0,this._lastRenderMs=0,this._panOffset=[0,0,0],this._panLimit=null,this._zoomFactor=1,this._zoomLimit=null,this._camMode=null,this._camModeType=null,this._maskFeather={},this._clips={},this._clipPlaybacks={},this._clipHeld={},this._clipMaskState={},this._clipMaskSoftEdge={},this._clipPartsRegistry={},this._clipPartsLoadQueue=Promise.resolve(),this._clipPartsFlush=null,this._urlCache=new Map,this._transitions={},this._transitionPlaybacks={},this._axisActive={},this._states={},this._stateFrame={},this._stateActive={},this._stateTarget={},this._statePlaybacks={}}setMaskFeather(t,e){this._maskFeather[t]=e}_mergeClipParts(t){return Object.assign(this._clipPartsRegistry,t),this._clipPartsFlush||(this._clipPartsFlush=new Promise((e,s)=>{setTimeout(()=>{this._clipPartsFlush=null;let i={...this._clipPartsRegistry},r=this._clipPartsLoadQueue.then(()=>this.loadParts(i),()=>this.loadParts(i));this._clipPartsLoadQueue=r.catch(()=>{}),r.then(e,s)},30)})),this._clipPartsFlush}async loadClips(t,e={}){let s=await fetch(t);if(!s.ok)throw new Error(`HoloSplat: failed to load clips "${t}" (HTTP ${s.status})`);let i=await s.json(),r=[];for(let n of i.clips??[]){let o=(n.masks??[]).map(l=>({name:l.name,softEdge:l.softEdge??.05,matrices:new Float32Array(l.matrices)}));this._clips[n.id]={id:n.id,fps:n.fps??i.fps??24,frameCount:n.frameCount,holdFrame:n.holdFrame,objects:(n.objects??[]).map(l=>({id:l.id,frames:new Float32Array(l.frames)})),masks:o},r.push(n.id);for(let l of o){if(this._clipMaskState[l.name])continue;let c=l.matrices.length/16-1;this._clipMaskState[l.name]=l.matrices.slice(c*16,c*16+16),this._clipMaskSoftEdge[l.name]=l.softEdge}}for(let[n,o]of Object.entries(i.transitions??{})){let l=(o.masks??[]).map(c=>({name:c.name,value:c.value,softEdge:c.softEdge??.05,matrices:new Float32Array(c.matrices)}));this._transitions[n]={fps:o.fps??i.fps??24,frameCount:o.frameCount,holdFrame:o.holdFrame,parts:(o.parts??[]).map(c=>({id:c.id,value:c.value,frames:new Float32Array(c.frames)})),masks:l};for(let c of l){if(this._clipMaskState[c.name])continue;let u=c.matrices.length/16-1;this._clipMaskState[c.name]=c.matrices.slice(u*16,u*16+16),this._clipMaskSoftEdge[c.name]=c.softEdge}}for(let[n,o]of Object.entries(i.states??{})){let l=(o.masks??[]).map(c=>({name:c.name,softEdge:c.softEdge??.05,matrices:new Float32Array(c.matrices)}));this._states[n]={fps:o.fps??i.fps??24,frameCount:o.frameCount,markers:o.markers??{},default:o.default,parts:(o.parts??[]).map(c=>({id:c.id,frames:new Float32Array(c.frames)})),masks:l}}if(e.splatsDir&&i.parts&&Object.keys(i.parts).length){let n=e.splatsDir.replace(/\/?$/,"/"),o={};for(let[l,c]of Object.entries(i.parts))o[l]=`${n}${c.splatName}${Xe(c,e.defaults)}.spz`;e.lod&&(o=await dt(o,e.lod)),await this._mergeClipParts(o),Object.values(i.parts).some(l=>l.variants.length>1)&&setTimeout(()=>{(async()=>{let l={};for(let[c,u]of Object.entries(i.parts)){if(!u.variants.length){l[c]=`${n}${u.splatName}.spz`;continue}let m=Xe(u,e.defaults).slice(1);l[c]=await Promise.all(u.variants.map(f=>{let d=f===m?e.lod||0:Math.max(e.lod||0,Ds);return et(`${n}${u.splatName}.${f}.spz`,d)}))}try{await this._mergeClipParts(l)}catch(c){console.error(`[HoloSplat] background variant load failed for "${t}":`,c)}})()},Us)}for(let[n,o]of Object.entries(e.defaults??{}))this._transitions[n]&&this._setVariantInstant(n,o),this._states[n]&&this._setStateInstant(n,o);for(let[n,o]of Object.entries(this._states))this._stateActive[n]===void 0&&o.default!=null&&this._setStateInstant(n,o.default);return r}unloadClips(t){for(let e of t)delete this._clips[e]}playClip(t){let e=this._clips[t];if(!e){console.warn(`[HoloSplat] playClip: unknown clip "${t}"`);return}let s=t.lastIndexOf("-");if(s<=0){console.warn(`[HoloSplat] playClip: "${t}" doesn't match "productName-variant"`);return}let i=t.slice(0,s),r=this._clipHeld[i];if(r===t)return;let n=r?this._clips[r]:null;this._clipPlaybacks[i]=n?{clip:n,dir:"out",frame:n.holdFrame,nextClipId:t}:{clip:e,dir:"in",frame:0,nextClipId:null},this._clipHeld[i]=t}_tickClips(t){for(let e in this._clipPlaybacks){let s=this._clipPlaybacks[e],{clip:i,dir:r}=s,n=r==="in"?i.holdFrame:i.frameCount-1;s.frame+=t*i.fps;let o=s.frame>=n;o&&(s.frame=n),this._applyClipFrame(i,s.frame),o&&(r==="out"&&s.nextClipId?this._clipPlaybacks[e]={clip:this._clips[s.nextClipId],dir:"in",frame:0,nextClipId:null}:delete this._clipPlaybacks[e])}}_applyClipFrame(t,e){let s=!1;for(let{id:i,pos:r,quat:n}of Ye(t,e)){let o=this._partIndex[i];if(o&&o.length){let l=Ft(r,n);this._partLocalPose[i]=l;for(let c of o)this._partTransFlat.set(l,c*16);s=!0}}for(let{name:i,softEdge:r,matrix:n}of We(t,e))this._clipMaskState[i]=n,this._clipMaskSoftEdge[i]=r,this._sortDirty=!0;s&&(this._renderer.updateTransforms(this._partTransFlat),this._sortDirty=!0)}playVariant(t,e){if(!this._transitions[t]){console.warn(`[HoloSplat] playVariant: unknown axis "${t}"`);return}let i=this._axisActive[t];i!==e&&(this._axisActive[t]=e,this._transitionPlaybacks[t]={value:e,prevValue:i,elapsed:0})}_setVariantInstant(t,e){let s=this._transitions[t];s&&(this._axisActive[t]=e,this._applyTransitionValue(s,e,s.holdFrame))}_tickTransitions(t){for(let e in this._transitionPlaybacks){let s=this._transitionPlaybacks[e],i=this._transitions[e];s.elapsed+=t*i.fps;let r=Math.min(s.elapsed,i.holdFrame),n=Math.min(i.holdFrame+s.elapsed,i.frameCount-1);this._applyTransitionValue(i,s.value,r),s.prevValue&&this._applyTransitionValue(i,s.prevValue,n),r>=i.holdFrame&&n>=i.frameCount-1&&delete this._transitionPlaybacks[e]}}_applyTransitionValue(t,e,s){let i=!1;for(let r of t.parts){if(r.value!==e)continue;let n=this._partIndex[r.id];if(n&&n.length){let{pos:o,quat:l}=le(r.frames,t.frameCount,s),c=Ft(o,l);this._partLocalPose[r.id]=c;for(let u of n)this._partTransFlat.set(c,u*16);i=!0}}for(let r of t.masks)r.value===e&&(this._clipMaskState[r.name]=ce(r.matrices,t.frameCount,s),this._clipMaskSoftEdge[r.name]=r.softEdge,this._sortDirty=!0);i&&(this._renderer.updateTransforms(this._partTransFlat),this._sortDirty=!0)}playState(t,e){let s=this._states[t];if(!s){console.warn(`[HoloSplat] playState: unknown state axis "${t}"`);return}if(!(e in s.markers)){console.warn(`[HoloSplat] playState: axis "${t}" has no value "${e}"`);return}if(this._stateTarget[t]===e)return;let i=s.markers[e],r=this._stateFrame[t]??i;if(this._stateTarget[t]=e,r===i){this._stateActive[t]=e,delete this._statePlaybacks[t];return}this._statePlaybacks[t]={dir:i>r?1:-1,frame:r,toFrame:i,value:e}}_setStateInstant(t,e){let s=this._states[t];if(!s||!(e in s.markers))return;let i=s.markers[e];this._stateFrame[t]=i,this._stateActive[t]=e,this._stateTarget[t]=e,delete this._statePlaybacks[t],this._applyStateFrame(s,i)}_tickStates(t){for(let e in this._statePlaybacks){let s=this._statePlaybacks[e],i=this._states[e];s.frame+=t*i.fps*s.dir;let r=s.dir>0?s.frame>=s.toFrame:s.frame<=s.toFrame;r&&(s.frame=s.toFrame),this._applyStateFrame(i,s.frame),this._stateFrame[e]=s.frame,r&&(this._stateActive[e]=s.value,delete this._statePlaybacks[e])}}_applyStateFrame(t,e){let s=!1;for(let i of t.parts){let r=this._partIndex[i.id];if(r&&r.length){let{pos:n,quat:o}=le(i.frames,t.frameCount,e),l=Ft(n,o);this._partLocalPose[i.id]=l;for(let c of r)this._partTransFlat.set(l,c*16);s=!0}}for(let i of t.masks)this._clipMaskState[i.name]=ce(i.matrices,t.frameCount,e),this._clipMaskSoftEdge[i.name]=i.softEdge,this._sortDirty=!0;s&&(this._renderer.updateTransforms(this._partTransFlat),this._sortDirty=!0)}_syncAssetStates(){let t=this._animation.stateCalls;if(!t||!t.length)return;let e=this._animation.frame,s={};for(let i of t)this._states[i.axis]&&i.frame<=e&&(!s[i.axis]||i.frame>s[i.axis].frame)&&(s[i.axis]=i);for(let i in this._states){let r=s[i],n=r?r.value:this._states[i].default;n!=null&&this.playState(i,n)}}async init(){await this._renderer.init(),this._renderer.setSplatScale(this._splatScale),this._renderer.setAaDilation(this._aaDilation),this._camera.attach(this._canvas),this._observeResize(),this._updateSize()}async load(t){if(this._lastUrl=t,this._lastParts=null,this._sceneReady=!1,t.split("?")[0].split(".").pop().toLowerCase()==="ply")try{await this._loadPlyStreamSingle(t);return}catch(l){if(!/^HTTP 4/.test(l.message))throw l}let{data:s,count:i,variants:r,shData:n,numSHBases:o}=await qt(t,l=>this._onProgress?.(l),this._shDegree);this._flipY&&(vt(s,i),Ct(n,o,i));for(let l=0;l<i;l++)s[l*16+3]=0;if(this._lastShData=n,this._lastNumSHBases=o,this._gaussians=s,this._numSplats=i,this._depths=new Float32Array(i),this._sort=_t(i),this._partIndex={},this._partLocalPose={},this._partVariants=r?{0:{kind:"spzv",variants:r,active:r[0]?.name}}:{},this._fileNames=[Lt(t)],this._partTransforms=[nt.slice()],this._partTransFlat=nt.slice(),this._fileRanges=[[0,i]],this._fileAABB=[null],this._activeIdx=null,this._renderer.uploadGaussians(s,i),this._renderer.uploadTransforms(this._partTransforms),this._renderer.uploadSH(n||null,o||0),this._renderer.setShDegree(o>0?this._shDegree:0),this._sortDirty=!0,this._camera.fitScene(s,i),this._animation){let{eye:l,target:c}=this._animation.getCameraFrame();this._camera.setFromLookAt(l,c)}this._buildPartVolumeMask(),this._sceneReady=!0}async _loadPlyStreamSingle(t){let{numVertices:e,numSHBases:s,consume:i}=await se(t,o=>this._onProgress?.(o*.02),this._shDegree),r=new Float32Array(e*16);if(this._gaussians=r,this._numSplats=e,this._depths=new Float32Array(e),this._sort=_t(e),this._partIndex={},this._partLocalPose={},this._partVariants={},this._fileNames=[Lt(t)],this._partTransforms=[nt.slice()],this._partTransFlat=nt.slice(),this._fileRanges=[[0,e]],this._fileAABB=[null],this._activeIdx=null,this._renderer.uploadGaussians(r,e),this._renderer.uploadTransforms(this._partTransforms),this._renderer.allocateSH(e,s),this._renderer.setShDegree(s>0?this._shDegree:0),this._buildPartVolumeMask(),this._animation){let{eye:o,target:l}=this._animation.getCameraFrame();this._camera.setFromLookAt(o,l)}this._lastShData=s>0?new Float32Array(e*s*3):null,this._lastNumSHBases=s;let n=0;await i((o,l,c)=>{for(let u=0;u<l;u++)o[u*16+3]=0;this._flipY&&(vt(o,l),Ct(c,s,l)),this._renderer.patchGaussians(o,n),this._renderer.patchSH(c,n),this._gaussians.set(o,n*16),this._lastShData&&c&&this._lastShData.set(c,n*s*3),n+=l},o=>this._onProgress?.(o)),this._animation||this._camera.fitScene(this._gaussians,this._numSplats),this._sceneReady=!0}async _loadUrlCached(t,e,s){let i=`${t}#${s}`,r=this._urlCache.get(i);r?e?.(1):(r=qt(t,e,s),this._urlCache.set(i,r),r.catch(()=>this._urlCache.delete(i)));let n=await r;return{...n,data:n.data.slice(),shData:n.shData?n.shData.slice():n.shData}}async loadParts(t){this._lastParts=t,this._lastUrl=null;let e=Object.keys(t);if(e.length===0)throw new Error("HoloSplat: loadParts called with empty map");this._sceneReady=!1;let s=[];e.forEach(h=>{let p=t[h];if(p&&typeof p=="object"&&!Array.isArray(p)){s.push({id:h,slot:s.length,url:p.url,variantNames:p.variants});return}let w=Array.isArray(p)?p:[p];for(let y of w)s.push({id:h,slot:s.length,url:y})});let i=new Array(s.length).fill(0),r=()=>{this._onProgress&&this._onProgress(i.reduce((h,p)=>h+p,0)/s.length)},n=await Promise.all(s.map(async({id:h,slot:p,url:w},y)=>{if(w.split("?")[0].split(".").pop().toLowerCase()==="ply")try{let{numVertices:P,numSHBases:M,consume:U}=await se(w,I=>{i[y]=I*.03,r()},this._shDegree);return{id:h,slot:p,kind:"stream",numVertices:P,numSHBases:M,consume:U}}catch(P){if(!/^HTTP 4/.test(P.message))throw P}let{data:S,count:_,variants:v,shData:B,numSHBases:b}=await this._loadUrlCached(w,P=>{i[y]=P*.9,r()},this._shDegree);return i[y]=.9,{id:h,slot:p,kind:"loaded",data:S,count:_,variants:v,shData:B,numSHBases:b}})),o=n.map(h=>h.kind==="stream"?h.numVertices:h.count),l=o.reduce((h,p)=>h+p,0),c=[];{let h=0;for(let p of o)c.push(h),h+=p}let u=new Float32Array(l*16),m=new Array(s.length).fill(null);n.forEach((h,p)=>{if(h.kind!=="loaded")return;let{data:w,count:y,slot:g,shData:S,numSHBases:_}=h;for(let v=0;v<y;v++)w[v*16+3]=g;this._flipY&&(vt(w,y),Ct(S,_,y)),u.set(w,c[p]*16),m[g]=fe(w,y),i[p]=1}),this._partIndex={},e.forEach(h=>{this._partIndex[h]=[]}),s.forEach(({id:h,slot:p})=>{this._partIndex[h].push(p)}),this._fileNames=s.map(h=>Lt(h.url)),this._partVariants={},n.forEach(h=>{h.variants&&(this._partVariants[h.slot]={kind:"spzv",variants:h.variants,active:h.variants[0]?.name})}),s.forEach(({slot:h,url:p,variantNames:w})=>{if(!w?.length||this._partVariants[h])return;let g=p.split("?")[0].match(/^(.*)\.([^./]+)\.(spz|ply|splat)$/i);!g||!w.includes(g[2])||(this._partVariants[h]={kind:"file",names:w,active:g[2],baseUrl:g[1],ext:g[3],cache:{}})}),this._partTransforms=s.map(()=>nt.slice()),this._partTransFlat=new Float32Array(s.length*16);for(let h=0;h<s.length;h++)this._partTransFlat.set(nt,h*16);this._fileRanges=c.map((h,p)=>[h,h+o[p]]),this._fileAABB=m,this._activeIdx=new Uint32Array(l),this._gaussians=u,this._numSplats=l,this._depths=new Float32Array(l),this._sort=_t(l),this._renderer.uploadGaussians(u,l),this._renderer.uploadTransforms(this._partTransforms);let f=n.reduce((h,p)=>Math.max(h,p.numSHBases||0),0);if(this._renderer.allocateSH(l,f),this._renderer.setShDegree(f>0?this._shDegree:0),n.forEach((h,p)=>{h.kind==="loaded"&&h.shData&&h.numSHBases===f&&this._renderer.patchSH(h.shData,c[p])}),this._buildPartVolumeMask(),this._camera.fitScene(u,l),this._animation){let{eye:h,target:p}=this._animation.getCameraFrame();this._camera.setFromLookAt(h,p)}r();let d=n.map((h,p)=>({part:h,i:p})).filter(({part:h})=>h.kind==="stream");if(d.length===0){this._sceneReady=!0,this._prefetchVariants();return}await Promise.all(d.map(async({part:h,i:p})=>{let{slot:w,consume:y}=h,g=c[p];await y((S,_,v)=>{for(let B=0;B<_;B++)S[B*16+3]=w;this._flipY&&(vt(S,_),Ct(v,h.numSHBases,_)),this._renderer.patchGaussians(S,g),v&&h.numSHBases===f&&this._renderer.patchSH(v,g),this._gaussians.set(S,g*16),this._fileAABB[w]=Gs(this._fileAABB[w],S,_),g+=_},S=>{i[p]=.03+.97*S,r()}),i[p]=1,r()})),this._animation||this._camera.fitScene(this._gaussians,this._numSplats),this._sceneReady=!0,this._prefetchVariants()}start(){this._running||(this._running=!0,this._tick())}stop(){this._running=!1,this._rafId&&cancelAnimationFrame(this._rafId),this._rafId=null}destroy(){this.stop(),this._camera.detach(),this._renderer.destroy(),this._resizeObs?.disconnect(),clearTimeout(this._resizeTimer)}setBackground(t){this._renderer.setBackground(t)}setSplatScale(t){this._splatScale=t,this._renderer.setSplatScale(t)}setGamma(t){this._renderer.setGamma(t)}async setShDegree(t){t!==this._shDegree&&(this._shDegree=t,this._lastParts?await this.loadParts(this._lastParts):this._lastUrl&&await this.load(this._lastUrl))}setAaDilation(t){this._aaDilation=t,this._renderer.setAaDilation(t),this._sortDirty=!0}setDebugIndex(t){this._renderer.setDebugIndex(t)}async readDebug(){return this._renderer.readDebug()}setAutoRotate(t){this._autoRotate=t}setFlipY(t){!!t!==this._flipY&&(this._flipY=!!t,this._gaussians&&(vt(this._gaussians,this._numSplats),this._renderer.uploadGaussians(this._gaussians,this._numSplats),this._camera.fitScene(this._gaussians,this._numSplats),this._sortDirty=!0,this._lastShData&&(Ct(this._lastShData,this._lastNumSHBases,this._numSplats),this._renderer.uploadSH(this._lastShData,this._lastNumSHBases))))}getStats(){return{fps:this._frameTimeEMA?1/this._frameTimeEMA:0,numSplats:this._numSplats,activeSplats:this._lastActiveCount||this._numSplats,shDegree:this._shDegree,pixelRatio:this._effectivePixelRatio,sceneName:this._currentSceneName,gpuSort:this._gpuSort&&!this._renderer._gpuSortFailed,gpuSortFailed:this._gpuSort&&this._renderer._gpuSortFailed,tier:this._tier}}getSplatDebug(t){if(!this._gaussians||t<0||t>=this._numSplats)return null;let e=t*16,s=this._gaussians;return{pos:[s[e],s[e+1],s[e+2]],part:s[e+3],color:[s[e+4],s[e+5],s[e+6],s[e+7]],scale:[s[e+8],s[e+9],s[e+10]],quat:[s[e+12],s[e+13],s[e+14],s[e+15]]}}getVariants(t){let e=t==null?[0]:this._partIndex[t];if(!e)return[];let s=this._partVariants[e[0]];return s?s.kind==="spzv"?s.variants.map(i=>i.name):s.names:[]}async setVariant(t,e){let s=t==null?[0]:this._partIndex[t];if(!s)return!1;let i=!1;for(let r of s){let n=this._partVariants[r];if(n){if(n.kind==="spzv"){let o=n.variants.find(u=>u.name===e);if(!o)continue;let[l,c]=this._fileRanges[r];for(let u=l;u<c;u++){let m=u*16,f=(u-l)*4;this._gaussians[m+4]=o.palette[f+0],this._gaussians[m+5]=o.palette[f+1],this._gaussians[m+6]=o.palette[f+2],this._gaussians[m+7]=o.palette[f+3]}this._renderer.patchGaussians(this._gaussians.subarray(l*16,c*16),l),n.active=e,i=!0;continue}if(n.active===e){i=!0;continue}n.names.includes(e)&&(await this._swapPartVariant(r,n,e),i=!0)}}return i}async _swapPartVariant(t,e,s){let i=e.cache[s];i||(i=await qt(`${e.baseUrl}.${s}.${e.ext}`),e.cache[s]=i);let[r,n]=this._fileRanges[t],o=n-r,l=i.count,c=l-o,u=i.data.slice();for(let m=0;m<l;m++)u[m*16+3]=t;if(this._flipY&&vt(u,l),c===0)this._gaussians.set(u,r*16),this._renderer.patchGaussians(u,r);else{let m=this._numSplats+c,f=new Float32Array(m*16);f.set(this._gaussians.subarray(0,r*16),0),f.set(u,r*16),f.set(this._gaussians.subarray(n*16),(r+l)*16);for(let d=0;d<this._fileRanges.length;d++){let[h,p]=this._fileRanges[d];d===t?this._fileRanges[d]=[r,r+l]:h>=n&&(this._fileRanges[d]=[h+c,p+c])}this._gaussians=f,this._numSplats=m,this._depths=new Float32Array(m),this._sort=_t(m),this._activeIdx=new Uint32Array(m),this._renderer.uploadGaussians(this._gaussians,this._numSplats)}this._fileAABB[t]=fe(u,l),this._fileNames[t]=Lt(`${e.baseUrl}.${s}.${e.ext}`),e.active=s,this._buildPartVolumeMask()}async _prefetchVariants(){if(this._prefetchVariantsEnabled){for(let t of Object.values(this._partVariants))if(t.kind==="file"){for(let e of t.names)if(!(e===t.active||t.cache[e]))try{t.cache[e]=await qt(`${t.baseUrl}.${e}.${t.ext}`)}catch(s){console.warn(`[HoloSplat] variant prefetch failed for "${t.baseUrl}.${e}.${t.ext}": ${s.message}`)}}}}setAnimationPaused(t){this._animPaused=t}setCameraFree(t){let e=this._cameraFree;this._cameraFree=!!t,t?(this._blendBack=null,this._camera.disableZoom(),this.animTickOverride&&(this._camera.allowTouchScroll=!1)):(this._camera.enableZoom(),this.animTickOverride&&(this._camera.allowTouchScroll=!0),e&&this._animation&&(this._blendBack={fromEye:this._camera.eye.slice(),fromTarget:this._camera.target.slice(),t:0,duration:.5}))}_syncCameraMode(){let t=this._animation.markers,e=this._animation.frame,s=null,i=-1;for(let[c,u]of Object.entries(t))u<=e&&u>i&&(i=u,s=c);let r=null;if(s&&(r=window.__hsSceneConfigs?.[s]??null,!r)){let c=Hs(s);if(c&&c!=="hs-locked"){let u=c.match(/zoom-(\d+)/);r={zoom:{enabled:!!u,mode:"limited",range:u?+u[1]:25}}}}let n=r?JSON.stringify(r):null;if(n===this._camMode)return;this._camMode=n;let o=r?`${+!!r.pan?.enabled}:${+!!r.zoom?.enabled}`:null,l=o!==this._camModeType;if(this._camModeType=o,r){let c=r.pan||{},u=r.zoom||{};if(l&&(this._panOffset=[0,0,0],this._zoomFactor=1),u.enabled?(this._camera.zoomEnabled=!0,this._zoomLimit=u.limited?Math.max(0,(u.range??500)/100):null,this._zoomLimit!==null&&(this._zoomFactor=Math.max(1-this._zoomLimit,Math.min(1+this._zoomLimit,this._zoomFactor))),this._camera.zoomDeltaCallback=m=>{let f=this._zoomFactor*m;this._zoomLimit!==null&&(f=Math.max(1-this._zoomLimit,Math.min(1+this._zoomLimit,f))),this._zoomFactor=Math.max(.01,f)}):(this._camera.zoomEnabled=!1,this._camera.zoomDeltaCallback=null,this._zoomFactor=1),c.enabled){if(this._camera.panEnabled=!0,this._camera.panSpeed=1-Math.min(100,Math.max(0,c.damping??0))/100,this._camera.panButton=c.button==="left"?0:2,this._panLimit=c.limited?Math.max(0,(c.radius??500)/100)*this._camera.radius:null,this._panLimit!==null){let m=this._panOffset,f=Math.hypot(m[0],m[1],m[2]);if(f>this._panLimit){let d=this._panLimit/f;m[0]*=d,m[1]*=d,m[2]*=d}}this._camera.panDeltaCallback=(m,f,d)=>{let h=this._panOffset;if(h[0]+=m,h[1]+=f,h[2]+=d,this._panLimit!==null){let p=Math.hypot(h[0],h[1],h[2]);if(p>this._panLimit){let w=this._panLimit/p;h[0]*=w,h[1]*=w,h[2]*=w}}}}else this._camera.panEnabled=!1,this._camera.panDeltaCallback=null,this._panOffset=[0,0,0]}else this._panOffset=[0,0,0],this._zoomFactor=1,this._camera.panDeltaCallback=null,this._camera.zoomDeltaCallback=null,this._camera.panEnabled=!this._animation.focalPoint,this._camera.panSpeed=1,this._camera.panButton=2,this._camera.panRadius=null,this._camera.panOrigin=null,this._camera.enableZoom()}resetCamera(){this._camera.fitScene(this._gaussians,this._numSplats)}focusCamera(){this._camera.focusScene(this._gaussians,this._numSplats)}setGaussians(t,e,s=!1){this._gaussians=t,this._numSplats=e,this._depths=new Float32Array(e),this._sort=_t(e),this._sceneReady=!0,this._renderer.uploadGaussians(t,e),s&&this._camera.fitScene(t,e)}uploadDisplay(t){this._numSplats&&this._renderer.uploadGaussians(t,this._numSplats)}_buildPartVolumeMask(){let t=new Set;for(let n of Object.values(this._clips))for(let o of n.masks??[])t.add(o.name);for(let n of Object.values(this._transitions))for(let o of n.masks??[])t.add(o.name);for(let n of Object.values(this._states))for(let o of n.masks??[])t.add(o.name);let e=[...this._animation?.volumes??[],...[...t].map(n=>({name:n}))],s=this._fileNames??[],i=Math.max(s.length,1),r=new Uint32Array(i);e.forEach((n,o)=>{let l=n.name,c=0;for(let u=0;u<s.length;u++)Os(s[u],l)&&(r[u]|=1<<o,c++);c===0&&s.length>0&&console.warn(`[HoloSplat] mask volume "${l}" matched 0 of ${s.length} file(s) \u2014 check naming convention`)}),this._fileMasks=r,this._renderer.uploadPartVolumeMask(r)}setAnimation(t){if(this._animation=t,!t)return;t.fov!=null&&(this._camera.fov=t.fov*Math.PI/180),t.near!=null&&(this._camera.near=t.near),t.far!=null&&(this._camera.far=t.far);let{eye:e,target:s}=t.getCameraFrame();this._camera.setFromLookAt(e,s),this._buildPartVolumeMask()}async loadAnimationUrl(t){let e=await he(t);return this.setAnimation(e),e}projectCallouts(t){let e=this._camera.viewMatrix,s=this._camera.projMatrix,i=this._canvas.clientWidth,r=this._canvas.clientHeight,n=[];for(let o of t){let[l,c,u]=o.pos,m=e[0]*l+e[4]*c+e[8]*u+e[12],f=e[1]*l+e[5]*c+e[9]*u+e[13],d=e[2]*l+e[6]*c+e[10]*u+e[14];if(d>=0){n.push({id:o.id,visible:!1,x:0,y:0});continue}let h=-d,p=(s[0]*m/h*.5+.5)*i,w=(1-(s[5]*f/h*.5+.5))*r;n.push({id:o.id,visible:!0,x:p,y:w})}return n}get camera(){return this._camera}_tick(){if(!this._running)return;this._rafId=requestAnimationFrame(()=>this._tick());let t=performance.now(),e=this._lastTick?Math.min((t-this._lastTick)/1e3,.1):0;this._lastTick=t,this._sceneReady&&!this._wasSceneReady&&(this._frameTimeEMA=null,this._qualityWarmupUntil=t+Ts),this._wasSceneReady=this._sceneReady;let s=!this._sceneReady||t<this._qualityWarmupUntil;this._updateAdaptiveQuality(e,s);let i=this._canvas.width,r=this._canvas.height,n=[],o=!1;if(Object.keys(this._clipPlaybacks).length>0&&this._tickClips(e),Object.keys(this._transitionPlaybacks).length>0&&this._tickTransitions(e),Object.keys(this._statePlaybacks).length>0&&this._tickStates(e),this._animation){if(!this._animPaused&&this._sceneReady&&(this.animTickOverride?this.animTickOverride(e):this._animation.tick(e)),!this._cameraFree){let{eye:y,target:g}=this._animation.getCameraFrame();if(this._blendBack){this._blendBack.t+=e;let S=Vs(Math.min(this._blendBack.t/this._blendBack.duration,1));this._camera.setFromLookAt(At(this._blendBack.fromEye,y,S),At(this._blendBack.fromTarget,g,S)),this._blendBack.t>=this._blendBack.duration&&(this._blendBack=null)}else{let S=y,_=g;if(this._sceneBlend){let{otherEye:v,otherTarget:B,bf:b}=this._sceneBlend;S=At(v,y,b),_=At(B,g,b)}this._camera.setFromLookAt(S,_),(this._panOffset[0]!==0||this._panOffset[1]!==0||this._panOffset[2]!==0)&&(this._camera.target[0]+=this._panOffset[0],this._camera.target[1]+=this._panOffset[1],this._camera.target[2]+=this._panOffset[2]),this._zoomFactor!==1&&(this._camera.radius*=this._zoomFactor)}}this._syncCameraMode(),this._syncAssetStates();let h=this._animation.getObjectFrames();if(h.length>0){let y=!1;for(let{id:g,pos:S,quat:_}of h){let v=this._partIndex[g];if(v&&v.length){let B=S,b=_,P=this._sceneBlend?.otherObjects?.[g];if(P){let U=this._sceneBlend.bf;B=At(P.pos,S,U),b=yt(P.quat[0],P.quat[1],P.quat[2],P.quat[3],_[0],_[1],_[2],_[3],U)}let M=Ft(B,b);this._partLocalPose[g]=M;for(let U of v)this._partTransFlat.set(M,U*16);y=!0}}y&&(this._renderer.updateTransforms(this._partTransFlat),o=!0)}let p=this._animation.getAnchorFrames();if(p.length){let y=!1;for(let{asset:g,pos:S,quat:_}of p){let v=Ft(S,_),B=`ctrl.${g}`;for(let b in this._partIndex){if(!b.startsWith(B))continue;let P=this._partLocalPose[b],M=P?Ze(v,P):v;for(let U of this._partIndex[b])this._partTransFlat.set(M,U*16);y=!0}}y&&(this._renderer.updateTransforms(this._partTransFlat),o=!0)}if(n=this._animation.getVolumeFrames(),this._sceneBlend?.otherVolumes){let{otherVolumes:y,bf:g}=this._sceneBlend;for(let S of n){let _=y[S.name];_&&(S.matrix=Ls(_,S.matrix,g))}}let w=this._animation.frame;n.length>0&&w!==this._lastVolAnimFrame&&(this._sortDirty=!0),this._lastVolAnimFrame=w}else this._autoRotate&&(this._camera.theta+=.005);for(let h in this._clipMaskState)n.push({name:h,matrix:this._clipMaskState[h],softEdge:this._clipMaskSoftEdge[h]});if(n.length>0){for(let h of n){let p=this._maskFeather[h.name];p!=null&&(h.softEdge=p)}this._renderer.updateMaskVolumes(n)}if(!this._numSplats)return;this._camera.update(i,r);let l=this._camera.viewMatrix,c=this._camera.projMatrix,u=!this._lastSortView;if(!u){for(let h=0;h<16;h++)if(l[h]!==this._lastSortView[h]){u=!0;break}}if(!u&&!o&&!this._sortDirty||t-this._lastRenderMs<15)return;this._lastRenderMs=t,this._lastSortView||(this._lastSortView=new Float32Array(16)),this._lastSortView.set(l),this._sortDirty=!1,this.onFrame&&this.onFrame(l,c,i,r);let m=this._camera.focalLength(r);this._renderer.updateUniforms({view:l,proj:c,width:i,height:r,focal:m,near:this._camera.near,radiusCap:this._effectiveRadiusCap});let f=this._computeActiveRanges(n,l,c),d=f===null?this._numSplats:f;if(this._lastActiveCount=d,this._renderer.preprocess(d),this._gpuSort&&f===null&&!this._renderer._gpuSortFailed)this._renderer.runGpuSort(d);else{this._computeDepths(l);let h=this._sort(this._depths,d,f===null?null:this._activeIdx);this._renderer.updateOrder(h,d)}this._renderer.draw(d)}_computeDepths(t){let e=t[2],s=t[6],i=t[10],r=t[14],n=[],o=this._partTransFlat,l=this._partTransforms.length;for(let f=0;f<l;f++){let d=f*16;n.push([e*o[d]+s*o[d+1]+i*o[d+2],e*o[d+4]+s*o[d+5]+i*o[d+6],e*o[d+8]+s*o[d+9]+i*o[d+10],e*o[d+12]+s*o[d+13]+i*o[d+14]+r])}let c=this._gaussians,u=this._depths,m=this._numSplats;for(let f=0;f<m;f++){let d=f*16,h=n[c[d+3]];u[f]=h[0]*c[d]+h[1]*c[d+1]+h[2]*c[d+2]+h[3]}}_computeActiveRanges(t,e,s){return null}_isFileOutsideView(t,e,s,i,r){let n=this._fileAABB[t];if(!n)return!1;let{min:o,max:l}=n,c=this._partTransFlat.subarray(t*16,t*16+16),u=this._camera.near,m=!0,f=!0,d=!0,h=!0,p=!0;for(let w=0;w<8;w++){let y=w&1?l[0]:o[0],g=w&2?l[1]:o[1],S=w&4?l[2]:o[2],_=c[0]*y+c[4]*g+c[8]*S+c[12],v=c[1]*y+c[5]*g+c[9]*S+c[13],B=c[2]*y+c[6]*g+c[10]*S+c[14],b=e[0]*_+e[4]*v+e[8]*B+e[12],P=e[1]*_+e[5]*v+e[9]*B+e[13],M=e[2]*_+e[6]*v+e[10]*B+e[14];if(M<-u){m=!1;let U=-M*i,I=-M*r;b>-U&&(f=!1),b<U&&(d=!1),P>-I&&(h=!1),P<I&&(p=!1)}else f=d=h=p=!1}return m||f||d||h||p}_isFileHidden(t,e,s){let i=this._fileMasks[t];if(!i)return!1;let r=this._fileAABB[t];if(!r)return!1;let n=this._partTransFlat.subarray(t*16,t*16+16);for(let o=0;o<e.length;o++){if(!(i>>o&1))continue;let l=s[o];if(!l)continue;let c=Ze(l,n),u=1/0,m=1/0,f=1/0,d=-1/0,h=-1/0,p=-1/0;for(let y=0;y<8;y++){let g=y&1?r.max[0]:r.min[0],S=y&2?r.max[1]:r.min[1],_=y&4?r.max[2]:r.min[2],v=c[0]*g+c[4]*S+c[8]*_+c[12],B=c[1]*g+c[5]*S+c[9]*_+c[13],b=c[2]*g+c[6]*S+c[10]*_+c[14];v<u&&(u=v),v>d&&(d=v),B<m&&(m=B),B>h&&(h=B),b<f&&(f=b),b>p&&(p=b)}let w=.5+(e[o].softEdge??.05);if(d<-w||u>w||h<-w||m>w||p<-w||f>w)return!0}return!1}_observeResize(){typeof ResizeObserver>"u"||(this._resizeObs=new ResizeObserver(()=>{clearTimeout(this._resizeTimer),this._resizeTimer=setTimeout(()=>this._updateSize(),150)}),this._resizeObs.observe(this._canvas))}_updateSize(){let t=Math.min(window.devicePixelRatio||1,this._effectivePixelRatio),e=Math.round(this._canvas.clientWidth*t),s=Math.round(this._canvas.clientHeight*t);e&&s&&(this._canvas.width!==e||this._canvas.height!==s)&&(this._canvas.width=e,this._canvas.height=s)}_updateAdaptiveQuality(t,e){if(!this._adaptiveQuality||!t)return;let s=1-Math.exp(-t/.5);this._frameTimeEMA=this._frameTimeEMA==null?t:this._frameTimeEMA+(t-this._frameTimeEMA)*s;let i=performance.now();if(i-this._lastQualityCheck<1e3)return;this._lastQualityCheck=i;let r=1/27,n=1/50;!e&&this._frameTimeEMA>r&&this._effectivePixelRatio>this._minPixelRatio?(this._effectivePixelRatio=Math.max(this._minPixelRatio,this._effectivePixelRatio*.85),this._updateSize()):this._frameTimeEMA<n&&this._effectivePixelRatio<this._maxPixelRatio&&(this._effectivePixelRatio=Math.min(this._maxPixelRatio,this._effectivePixelRatio*1.05),this._updateSize()),!e&&this._frameTimeEMA>r&&this._effectiveRadiusCap>this._minRadiusCap?this._effectiveRadiusCap=Math.max(this._minRadiusCap,this._effectiveRadiusCap*.85):this._frameTimeEMA<n&&this._effectiveRadiusCap<this._maxRadiusCap&&(this._effectiveRadiusCap=Math.min(this._maxRadiusCap,this._effectiveRadiusCap*1.05))}},nt=new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),Ts=8e3,Us=4e3,Ds=2;function Lt(a){return a.split("?")[0].split("/").pop().replace(/\.(spz|ply|splat)$/i,"").replace(/\.lod\d+$/i,"")}function Xe(a,t){return a.variants.length?"."+(a.variants.find(s=>{let i=s.indexOf("=");return i<0?!1:t?.[s.slice(0,i)]===s.slice(i+1)})??a.variants[0]):""}function Ze(a,t){let e=new Float32Array(16);for(let s=0;s<4;s++)for(let i=0;i<4;i++)e[s*4+i]=a[i]*t[s*4]+a[i+4]*t[s*4+1]+a[i+8]*t[s*4+2]+a[i+12]*t[s*4+3];return e}var Rs=3;function fe(a,t){if(t===0)return null;let e=1/0,s=1/0,i=1/0,r=-1/0,n=-1/0,o=-1/0;for(let l=0;l<t;l++){let c=l*16,u=a[c],m=a[c+1],f=a[c+2],d=Rs*Math.max(a[c+8],a[c+9],a[c+10]);u-d<e&&(e=u-d),u+d>r&&(r=u+d),m-d<s&&(s=m-d),m+d>n&&(n=m+d),f-d<i&&(i=f-d),f+d>o&&(o=f+d)}return{min:[e,s,i],max:[r,n,o]}}function Gs(a,t,e){let s=fe(t,e);return s?a?{min:[Math.min(a.min[0],s.min[0]),Math.min(a.min[1],s.min[1]),Math.min(a.min[2],s.min[2])],max:[Math.max(a.max[0],s.max[0]),Math.max(a.max[1],s.max[1]),Math.max(a.max[2],s.max[2])]}:s:a}function Os(a,t){let e=t.indexOf(".."),s=e===-1?t:t.slice(0,e),i=e===-1?"":t.slice(e+2),r=s===""||a===s||a.startsWith(s+"."),n=i===""||a===i||a.endsWith("."+i);return r&&n}function Hs(a){if(a.startsWith("hs-"))return a;let t=a.trim().split(/\s+/).filter(e=>e.startsWith("hs-"));return t.length?t.length===1?t[0]:"hs-"+t.map(e=>e.slice(3)).join("-"):null}function vt(a,t){for(let e=0;e<t;e++){let s=e*16;a[s+1]=-a[s+1],a[s+2]=-a[s+2];let i=a[s+12],r=a[s+13],n=a[s+14],o=a[s+15];a[s+12]=o,a[s+13]=-n,a[s+14]=r,a[s+15]=-i}}var Ke=[-1,-1,1,-1,1,1,-1,1,-1,1,-1,-1,1,-1,1];function Ct(a,t,e){if(!a||!t)return;let s=Math.min(t,Ke.length);for(let i=0;i<e;i++){let r=i*t*3;for(let n=0;n<s;n++){if(Ke[n]!==-1)continue;let o=r+n*3;a[o]=-a[o],a[o+1]=-a[o+1],a[o+2]=-a[o+2]}}}var ue=["spz","ply","splat"],Is={};async function qt(a,t,e=0){let s=a.split("?")[0],i=s.lastIndexOf("."),r=i>=0?s.slice(i+1).toLowerCase():"";if(r==="spzv")return $e(a,t);let n=ue.includes(r),o=n?[r,...ue.filter(u=>u!==r)]:ue,l=n?a.slice(0,a.lastIndexOf(".")):a,c;for(let u of o){let m=`${l}.${u}`,f=u==="ply"?(d,h)=>Ue(d,h,e):u==="spz"?(d,h)=>Ve(d,h,e):Is[u]??ze;try{return await f(m,t)}catch(d){if(!/^HTTP 4/.test(d.message))throw d;c=d}}throw new Error(`HoloSplat: splat file not found as .spz / .ply / .splat \u2014 "${l}"`)}function Vs(a){return a*a*(3-2*a)}function At(a,t,e){return[a[0]+(t[0]-a[0])*e,a[1]+(t[1]-a[1])*e,a[2]+(t[2]-a[2])*e]}function Ls(a,t,e){let s=new Float32Array(16);for(let i=0;i<16;i++)s[i]=a[i]+(t[i]-a[i])*e;return s}function Ft(a,t){let[e,s,i,r]=t,[n,o,l]=a,c=e*2,u=s*2,m=i*2,f=e*c,d=e*u,h=e*m,p=s*u,w=s*m,y=i*m,g=r*c,S=r*u,_=r*m;return new Float32Array([1-p-y,d+_,h-S,0,d-_,1-f-y,w+g,0,h+S,w-g,1-f-p,0,n,o,l,1])}function qs(a){if(!a)throw new Error("HoloSplat: canvas option is required");if(typeof a=="string"){let t=document.querySelector(a);if(!t)throw new Error(`HoloSplat: canvas selector "${a}" not found`);return t}return a}var js=`
|
|
703
|
+
.hs-player{position:relative;overflow:hidden;}
|
|
704
|
+
.hs-player canvas{position:absolute;inset:0;width:100%;height:100%;display:block;}
|
|
705
|
+
.hs-player .hs-overlay{
|
|
706
|
+
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
707
|
+
align-items:center;justify-content:center;gap:10px;
|
|
708
|
+
pointer-events:none;font-family:system-ui,sans-serif;
|
|
709
|
+
}
|
|
710
|
+
.hs-player .hs-pct{font-size:1.1rem;font-weight:600;color:rgba(255,255,255,.85);letter-spacing:.02em;}
|
|
711
|
+
.hs-player .hs-bar-wrap{width:140px;height:3px;background:rgba(255,255,255,.1);border-radius:2px;overflow:hidden;}
|
|
712
|
+
.hs-player .hs-bar{height:100%;background:#3a7aff;width:0%;transition:width .1s;}
|
|
713
|
+
.hs-player .hs-msg{font-size:.78rem;color:rgba(255,255,255,.4);text-align:center;max-width:260px;line-height:1.5;padding:0 16px;}
|
|
714
|
+
.hs-player .hs-msg.hs-err{color:#f87;}
|
|
715
|
+
.hs-player .hs-callouts{position:absolute;inset:0;pointer-events:none;}
|
|
716
|
+
.hs-lines{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;}
|
|
717
|
+
.hs-dot{fill:#3a7aff;stroke:#fff;stroke-width:2;}
|
|
718
|
+
.hs-line{stroke:rgba(255,255,255,.55);stroke-width:1.5;}
|
|
719
|
+
.hs-callout{position:absolute;pointer-events:auto;}
|
|
720
|
+
.hs-callout--hidden{display:none;}
|
|
721
|
+
`,Je=!1;function Ns(){if(Je||typeof document>"u")return;Je=!0;let a=document.createElement("style");a.textContent=js,document.head.appendChild(a)}function es(a,t={}){Ns();let e=typeof a=="string"?document.querySelector(a):a;if(!e)throw new Error(`HoloSplat: container not found \u2014 "${a}"`);let s=e.getAttribute("data-holosplat")||void 0,i=e.getAttribute("data-holosplat-anim")||void 0,{scene:r,src:n,parts:o,animation:l=i,clips:c,partsDir:u,partsExt:m="",scenes:f,masks:d,sh:h,background:p="transparent",fov:w=60,near:y=.01,far:g=2e3,splatScale:S=1.08,autoRotate:_=!1,flipY:v=!1,maxPixelRatio:B,quality:b="auto",adaptiveQuality:P=!0,prefetchVariants:M,aaDilation:U=.3,gpuSort:I=!1,zIndex:V=5,onLoad:z,onProgress:T,onError:F}=t,Y=r??n??s,rt=b==="auto"?It():b,j=Vt(rt),xt=B??j.maxPixelRatio;e.style.zIndex=String(V),e.classList.add("hs-player");let W=document.createElement("canvas"),$=document.createElement("div");$.className="hs-callouts";let L=document.createElement("div");L.className="hs-overlay",L.innerHTML='<div class="hs-pct">0%</div><div class="hs-bar-wrap"><div class="hs-bar"></div></div><div class="hs-msg"></div>',e.appendChild(W),e.appendChild($),e.appendChild(L);let Q=L.querySelector(".hs-pct"),ot=L.querySelector(".hs-bar-wrap"),Nt=L.querySelector(".hs-bar"),wt=L.querySelector(".hs-msg"),$t=null,Et=0;function Yt(){Et++,Q.textContent="0%",Q.style.display="",ot.style.display="",wt.textContent="",wt.className="hs-msg",L.style.display="flex",$t=performance.now()}function zt(){Et=Math.max(0,Et-1),Et===0&&(L.style.display="none")}function Wt(x){Q.style.display="none",ot.style.display="none",wt.textContent=x,wt.className="hs-msg hs-err",L.style.display="flex"}L.style.display="none";let C=new bt({canvas:W,background:p,fov:w,near:y,far:g,splatScale:S,autoRotate:_,flipY:v,aaDilation:U,maxPixelRatio:xt,adaptiveQuality:P,prefetchVariants:M??j.prefetchVariants,gpuSort:I,tier:rt,onProgress:x=>{let k=Math.round(x*100);if(Q.textContent=k+"%",Nt.style.width=k+"%",$t&&x>.05){let G=(performance.now()-$t)/1e3,O=Math.round(G*(1-x)/x);wt.textContent=O>0?"~"+O+"s":""}T&&T(x)}}),St={},Qt="http://www.w3.org/2000/svg";function os(x){$.innerHTML="";for(let G of Object.keys(St))delete St[G];if(!x.length)return;let k=document.createElementNS(Qt,"svg");k.setAttribute("class","hs-lines"),$.appendChild(k);for(let G of x){let O=document.createElementNS(Qt,"line");O.setAttribute("class","hs-line"),k.appendChild(O);let R=document.createElementNS(Qt,"circle");R.setAttribute("class","hs-dot"),R.setAttribute("r","5"),k.appendChild(R);let N=e.querySelector(`.hs-callout[data-id="${G.id}"]`)??document.querySelector(`.hs-callout[data-id="${G.id}"]`);N||(N=document.createElement("div"),N.className="hs-callout",N.dataset.id=G.id),$.appendChild(N),St[G.id]={card:N,dot:R,line:O}}}C.onFrame=()=>{if(!C._animation?.callouts.length)return;let x=C.projectCallouts(C._animation.callouts);for(let{id:k,visible:G,x:O,y:R}of x){let N=St[k];if(!N)continue;let{card:Z,dot:st,line:J}=N;if(G){let Tt=parseFloat(Z.dataset.offsetX??Z.dataset.ox??80),Pt=parseFloat(Z.dataset.offsetY??Z.dataset.oy??-40),mt=O+Tt,Bt=R+Pt;st.setAttribute("cx",O),st.setAttribute("cy",R),J.setAttribute("x1",O),J.setAttribute("y1",R),J.setAttribute("x2",mt),J.setAttribute("y2",Bt),st.style.display="",J.style.display="",Z.style.left=mt+"px",Z.style.top=Bt+"px",Z.classList.remove("hs-callout--hidden")}else st.style.display="none",J.style.display="none",Z.classList.add("hs-callout--hidden")}};async function me(x){Yt(),Nt.style.width="0%";try{await C.load(x),zt()}catch(k){let G=navigator.gpu?k.message:"WebGPU not supported. Use Chrome 113+ or Edge 113+.";throw Wt(G),F&&F(k),k}}async function Xt(x){Yt(),Nt.style.width="0%";try{await C.loadParts(x),zt()}catch(k){let G=navigator.gpu?k.message:"WebGPU not supported. Use Chrome 113+ or Edge 113+.";throw Wt(G),F&&F(k),k}}function pe(){let x=Object.keys(C._clips),k=0;for(let G of x){let O=document.getElementById(G);!O||O.dataset.hsClipBound||(O.dataset.hsClipBound="1",O.addEventListener("click",()=>C.playClip(G)),k++)}k&&console.log(`[HoloSplat] ${k} clip button(s) bound:`,x)}async function ls(x){let{url:k,splatsDir:G,defaults:O}=typeof x=="string"?{url:x}:x;Yt();try{await C.loadClips(k,{splatsDir:G,defaults:O,lod:j.lod}),pe()}catch(R){console.error("[HoloSplat] clips failed to load:",R),F&&F(R)}finally{zt()}}async function Zt(x){try{let k=await C.loadAnimationUrl(x);return os(k.callouts),console.log(`[HoloSplat] animation loaded: ${k.frameCount} frames @ ${k.fps}fps, ${k.callouts.length} callout(s):`,k.callouts.map(G=>G.id),"| markers:",k.markers),k}catch(k){console.error("[HoloSplat] animation failed to load:",k),F&&F(k)}}let _e={load:me,loadParts:Xt,loadAnim:Zt,destroy(){C.destroy(),e.innerHTML="",e.classList.remove("hs-player")},setBackground(x){C.setBackground(x)},setSplatScale(x){C.setSplatScale(x)},setAutoRotate(x){C.setAutoRotate(x)},setFlipY(x){C.setFlipY(x)},setShDegree(x){return C.setShDegree(x)},setAaDilation(x){C.setAaDilation(x)},setAnimationPaused(x){C.setAnimationPaused(x)},setCameraFree(x){C.setCameraFree(x)},setMaskFeather(x,k){C.setMaskFeather(x,k)},playClip(x){C.playClip(x)},playVariant(x,k){C.playVariant(x,k)},playState(x,k){C.playState(x,k)},async loadClips(x,k){let G=await C.loadClips(x,k);return pe(),G},unloadClips(x){C.unloadClips(x)},getVariants(x){return C.getVariants(x)},setVariant(x,k){return C.setVariant(x,k)},resetCamera(){C.resetCamera()},callout(x){return St[x]?.card??null},get camera(){return C.camera},get animation(){return C._animation},get animationPaused(){return C._animPaused},get clips(){return C._clips}};window.__hsPlayers||(window.__hsPlayers=[]),window.__hsPlayers.push({root:e,api:_e,viewer:C});function cs(x){if(!f||!x)return null;history.scrollRestoration&&(history.scrollRestoration="manual"),window.scrollTo(0,0),document.documentElement.style.overflowAnchor="none";let k=15,G=.12,O=Object.entries(x.markers).sort((A,D)=>A[1]-D[1]),R=O.map(([A,D],E)=>{let H=f[A]??{};return{name:A,fromFrame:D,toFrame:(O[E+1]?.[1]??x.frameCount)-1,playback:H.playback??"scroll",pingpong:H.pingpong??!1,playOnce:H.playOnce??!1,blendIn:(H.blendIn??0)/100,blendOut:(H.blendOut??0)/100,done:!1,el:H.linkedId?document.getElementById(H.linkedId):null}});if(R.forEach((A,D)=>{A.next=R[D+1]??null}),!R.some(A=>A.playback!=="auto"))return null;C.camera.allowTouchScroll=!0;function Z(A){if(!A.el)return null;let D=A.el.getBoundingClientRect();if(D.top>0)return null;let E=window.scrollY,H=E+D.top,tt=H+Math.max(A.el.offsetHeight,1),pt=document.documentElement.scrollHeight-window.innerHeight,K=Math.min(tt,Math.max(pt,H+1)),Pe=Math.max(0,Math.min(1,(E-H)/(K-H)));return{t:Pe,frame:A.fromFrame+Pe*(A.toFrame-A.fromFrame)}}let st=null,J=null,Tt=null,Pt=null,mt=null;function Bt(A){let D=Math.max(0,Math.min(1,A));return D*D*(3-2*D)}function us(){let A=x.getCameraFrame();J=A.eye,Tt=A.target,Pt={};for(let D of x.getObjectFrames())Pt[D.id]={pos:D.pos,quat:D.quat};mt={};for(let D of x.getVolumeFrames())mt[D.name]=D.matrix}function fs(A,D){if(st!==A.name&&(st=A.name,us()),A.blendIn>0&&D<A.blendIn){let E=Bt(D/A.blendIn);C._sceneBlend={otherEye:J,otherTarget:Tt,otherObjects:Pt,otherVolumes:mt,bf:E};return}if(A.blendOut>0&&A.next&&D>1-A.blendOut){let E=Bt((D-(1-A.blendOut))/A.blendOut),H=x.getCameraFrame(A.next.fromFrame),tt={};for(let K of x.getObjectFrames(A.next.fromFrame))tt[K.id]={pos:K.pos,quat:K.quat};let pt={};for(let K of x.getVolumeFrames(A.next.fromFrame))pt[K.name]=K.matrix;C._sceneBlend={otherEye:H.eye,otherTarget:H.target,otherObjects:tt,otherVolumes:pt,bf:1-E};return}C._sceneBlend=null}let it=null,ye=!1,ve=null,be=R.findIndex(A=>A.playback==="scroll"),xe=be>0?R[be-1]:null,ds=xe?xe.toFrame:R[0]?.fromFrame??0;function we(){let A=null;for(let D of R){if(D.playback!=="scroll"||!D.el)continue;let E=Z(D);E!==null&&(A={scene:D,t:E.t,frame:E.frame})}if(A){fs(A.scene,A.t),it={scene:A.scene,frame:A.frame};return}it&&(it=null,st=null,C._sceneBlend=null,x.seekFrame(ve??ds))}function Se(){ye=!0,we()}return window.addEventListener("scroll",Se,{passive:!0}),we(),C.animTickOverride=A=>{let D=x.frame,E=null;for(let H of R)if(!(H.playOnce&&H.done&&D>=H.toFrame-.5)&&D<=H.toFrame+.5){E=H;break}if(E||(E=R[R.length-1]),!E){x.tick(A);return}if(C._currentSceneName=E.name,E.playback==="auto"){if(!E.playOnce&&ye&&it&&it.frame>E.toFrame+.5){ve=D,x.seekFrame(it.frame);return}if(E.playOnce&&E.done)return;let H=A;if(E.pingpong){let pt=Math.min(D-E.fromFrame,E.toFrame-D),K=Math.max(G,Math.min(1,pt/k));H=A*K}x.tick(H);let tt=x.frame;E.pingpong?tt>=E.toFrame?(x.seekFrame(E.toFrame),E.playOnce?E.done=!0:x.direction=-1):tt<=E.fromFrame&&(x.seekFrame(E.fromFrame),x.direction=1):tt<E.fromFrame?x.seekFrame(E.fromFrame):E.playOnce&&tt>=E.toFrame&&(x.seekFrame(E.toFrame),E.done=!0);return}it?x.seekFrame(it.frame):E.el||x.seekFrame(E.fromFrame)},()=>{window.removeEventListener("scroll",Se),C.animTickOverride=null,C._currentSceneName=null,C._sceneBlend=null}}if(f&&typeof f=="object"&&(window.__hsSceneConfigs=window.__hsSceneConfigs||{},Object.assign(window.__hsSceneConfigs,f)),d&&typeof d=="object"){window.__hsMaskConfigs=window.__hsMaskConfigs||{},Object.assign(window.__hsMaskConfigs,d);for(let[x,k]of Object.entries(d))k&&typeof k.feather=="number"&&C.setMaskFeather(x,k.feather)}let hs=h??(f?Object.values(f).reduce((x,k)=>Math.max(x,k.sh??0),C._shDegree):C._shDegree),ge=Math.min(hs,j.shDegreeCap);if(ge!==C._shDegree&&(C._shDegree=ge),C.init().then(async()=>{C.start();let x,k=c?Array.isArray(c)?c:[c]:[],G=Promise.all(k.map(ls));if(u&&l){if(x=await Zt(l),x?.objects.length){let O=u.replace(/\/?$/,"/"),R=Object.fromEntries(x.objects.map(N=>[N.id,`${O}${oe(N.id)}${m}`]));R=await dt(R,j.lod),await Xt(R)}}else{let O=[];o?O.push(dt(o,j.lod).then(Xt)):Y&&O.push(et(Y,j.lod).then(me)),l&&O.push(Zt(l).then(R=>{x=R})),await Promise.all(O)}await G,zt(),z?.(),cs(x??C._animation)}).catch(x=>{navigator.gpu||Wt("WebGPU not supported. Use Chrome 113+ or Edge 113+.")}),typeof location<"u"&&new URLSearchParams(location.search).has("hs")&&!document.getElementById("__hs-script")){let x=window.matchMedia("(pointer: coarse)").matches||window.matchMedia("(max-width: 768px)").matches||/Android|iPhone|iPad|iPod/i.test(navigator.userAgent||""),k=document.createElement("script");k.id="__hs-script",k.src=x?"/holosplat/stats.js":"/holosplat/editor.js",document.head.appendChild(k)}return _e}function ts(){document.querySelectorAll("[data-holosplat]").forEach(a=>{if(a._hsPlayer)return;let t=a.getAttribute("data-holosplat")||void 0,e=a.getAttribute("data-holosplat-anim")||void 0,s=a.getAttribute("data-holosplat-parts")||void 0;a._hsPlayer=es(a,{src:t,animation:e,partsDir:s})})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",ts):ts());var $s=`
|
|
722
|
+
.hs-scene {
|
|
723
|
+
position: relative;
|
|
724
|
+
width: 100%;
|
|
725
|
+
}
|
|
726
|
+
/* .hs-stage also carries .hs-player (position:relative) from player.js.
|
|
727
|
+
!important ensures sticky wins regardless of injection order.
|
|
728
|
+
100vw fills the full viewport regardless of parent padding/margin. */
|
|
729
|
+
.hs-stage {
|
|
730
|
+
position: sticky !important;
|
|
731
|
+
top: 0;
|
|
732
|
+
left: 0;
|
|
733
|
+
width: 100vw !important;
|
|
734
|
+
height: 100vh !important;
|
|
735
|
+
z-index: 1;
|
|
736
|
+
overflow: hidden;
|
|
737
|
+
}
|
|
738
|
+
.hs-stage canvas {
|
|
739
|
+
position: absolute !important;
|
|
740
|
+
inset: 0 !important;
|
|
741
|
+
width: 100% !important;
|
|
742
|
+
height: 100% !important;
|
|
743
|
+
display: block;
|
|
744
|
+
}
|
|
745
|
+
.hs-track {
|
|
746
|
+
/* Overlap the sticky stage so acts start scrolling at the same time the
|
|
747
|
+
canvas appears \u2014 not after it. */
|
|
748
|
+
margin-top: -100vh;
|
|
749
|
+
position: relative;
|
|
750
|
+
/* Clicks pass through to the canvas; restore per-element as needed. */
|
|
751
|
+
pointer-events: none;
|
|
752
|
+
z-index: 2;
|
|
753
|
+
}
|
|
754
|
+
.hs-act,
|
|
755
|
+
.hs-hold {
|
|
756
|
+
position: relative;
|
|
757
|
+
}
|
|
758
|
+
.hs-caption {
|
|
759
|
+
pointer-events: auto;
|
|
760
|
+
}
|
|
761
|
+
.hs-caption--hidden {
|
|
762
|
+
opacity: 0;
|
|
763
|
+
pointer-events: none;
|
|
764
|
+
}
|
|
765
|
+
`,ss=!1;function Ys(){if(ss||typeof document>"u")return;ss=!0;let a=document.createElement("style");a.textContent=$s,document.head.appendChild(a)}function de(a,t,e=0){if(a==null||a==="")return e;let s=String(a).trim();if(Object.prototype.hasOwnProperty.call(t,s))return t[s];let i=parseFloat(s);return isNaN(i)?(console.warn(`[HoloSplat] scrollScene: unknown marker "${s}" \u2014 using ${e}`),e):i}function Ws(a,t,e){if(a.classList.contains("hs-hold"))return{el:a,type:"hold",frame:de(a.dataset.frame,t,0),captions:is(a)};let i=a.dataset.from??"",r=i==="pingpong-start",n=i==="freecamera-start",o=de(i,t,0),l=de(a.dataset.to,t,e),c=0;if(a.dataset.loop!==void 0){let u=a.dataset.loop.trim();c=u===""||u==="true"?1:Math.max(1,parseFloat(u)||1)}return{el:a,type:r?"pingpong":n?"freecamera":"act",from:o,to:l,loop:c,captions:is(a)}}function is(a){return[...a.querySelectorAll(".hs-caption")].map(t=>({el:t,at:parseFloat(t.dataset.at??"0")}))}function as(a,t){if(a.type==="hold")return a.frame;if(a.type==="pingpong"||a.type==="freecamera")return t>=1?a.to:a.from;let{from:e,to:s,loop:i}=a,r=s-e;if(r===0)return e;let n=t;return i>0&&(n=t*i%1),e+n*r}function Qs(a){let t=a.getBoundingClientRect();if(t.top>0)return 0;let e=window.scrollY,s=e+t.top,i=a.offsetHeight||1,r=s+i,n=document.documentElement.scrollHeight-window.innerHeight,o=Math.min(r,Math.max(n,s+1));return Math.max(0,Math.min(1,(e-s)/(o-s)))}function Xs(a,t,e){return a+(t-a)*e}function Zs(a,t,e){let s=Math.max(0,Math.min(1,(e-a)/(t-a)));return s*s*(3-2*s)}function rs(a,t,e={}){Ys();let s=a.querySelector(".hs-track");if(!s)return console.warn("[HoloSplat] scrollScene: .hs-track not found inside scene"),{rebuild(){},destroy(){}};let i=[],r=!1,n=1,o=0,l=0,c=0,u=null,m=null,f=e.pingpongTransition??.25,d=!1,h=0,p=!1;function w(b,P,M=!1){r||(d=!1,r=!0,l=b,c=P,n=M?-1:1,o=M?P:b,u=null,m=requestAnimationFrame(g))}function y(){r=!1,m!==null&&(cancelAnimationFrame(m),m=null)}function g(b){if(!r)return;if(m=requestAnimationFrame(g),u===null){u=b;return}let P=(b-u)/1e3;u=b;let M=t.animation?.fps??24;o+=n*M*P,o>=c&&(o=c,n=-1),o<=l&&(o=l,n=1),t.animation.seekFrame(o)}function S(){let b=t.animation,P=b?.markers??{},M=b?b.frameCount-1:0;i=[...s.children].map(U=>Ws(U,P,M)),b&&t.setAnimationPaused(!0)}function _(){if(!t.animation||!i.length)return;let b=as(i[0],0),P=i[0],M=0;for(let z of i){let T=Qs(z.el);if(T<=0||(b=as(z,T),P=z,M=T,T<1))break}let U=P.type==="pingpong"&&M>0&&M<1,I=P.type==="freecamera"&&M>0&&M<1;if(U?(d=!1,r||w(P.from,P.to,M>.5)):(r&&(h=o,d=!0),y()),I?p||(p=!0,t.setCameraFree(!0),t.camera.enabled=!0,t.camera.panEnabled=!1):p&&(p=!1,t.setCameraFree(!1),t.camera.enabled=!1,t.camera.panEnabled=!0),!U&&!I)if(d){let z=Zs(0,f,M);z>=1&&(d=!1),t.animation.seekFrame(Xs(h,b,z))}else t.animation.seekFrame(b);let V=P?.el?.id??"\u2014";if(_._activeId!==V){_._activeId=V;let z=P,T=z.type==="hold"?`hold @ frame ${z.frame}`:z.type==="pingpong"?`pingpong [${z.from} \u2194 ${z.to}]`:`act [${z.from} \u2192 ${z.to}]`;console.log(`[HoloSplat] active: ${V} (${T}) | current frame: ${b.toFixed(1)}`)}for(let z of i){if(!z.captions.length)continue;let T=z===P;for(let F of z.captions){let Y=T&&M>=F.at;F.el.classList.toggle("hs-caption--hidden",!Y)}}}let v=!1;function B(){v||(v=!0,requestAnimationFrame(()=>{v=!1,_()}))}return window.addEventListener("scroll",B,{passive:!0}),function b(){if(!t.animation){requestAnimationFrame(b);return}t.camera.enabled=!1,e.onReady&&e.onReady(t.animation),S(),_()}(),{rebuild(){y(),d=!1,p&&(p=!1,t.setCameraFree(!1),t.camera.panEnabled=!0),S(),_()},destroy(){y(),d=!1,p&&(p=!1,t.setCameraFree(!1),t.camera.panEnabled=!0),window.removeEventListener("scroll",B),t.camera.enabled=!0,t.setAnimationPaused(!1)}}}function ns(){document.querySelectorAll(".hs-scene").forEach(a=>{if(a._hsScroll)return;let t=a.querySelector(".hs-stage");t&&t._hsPlayer&&(a._hsScroll=rs(a,t._hsPlayer))})}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",ns):ns());function jt(a,t){let e=t*16,s=a[e+7],i=a[e+8],r=a[e+9],n=a[e+10];return s*Math.cbrt(Math.max(0,i*r*n))}function Ks(a,t,e={}){let{minAlpha:s=0,minScale:i=0,keepFraction:r=1}=e,n=[];for(let l=0;l<t;l++){let c=l*16;a[c+7]<s||Math.max(a[c+8],a[c+9],a[c+10])<i||n.push(l)}r<1&&n.length>0&&(n.sort((l,c)=>jt(a,c)-jt(a,l)),n.length=Math.max(1,Math.round(n.length*r)));let o=new Float32Array(n.length*16);for(let l=0;l<n.length;l++)o.set(a.subarray(n[l]*16,n[l]*16+16),l*16);return{data:o,count:n.length}}function Js(a,t,e={}){let{minAlpha:s=0,minScale:i=0,fractions:r=[1,.8,.6,.4]}=e,n=[];for(let o=0;o<t;o++){let l=o*16;a[l+7]<s||Math.max(a[l+8],a[l+9],a[l+10])<i||n.push(o)}return n.sort((o,l)=>jt(a,l)-jt(a,o)),r.map(o=>{let l=Math.max(1,Math.round(n.length*o)),c=new Float32Array(l*16);for(let u=0;u<l;u++)c.set(a.subarray(n[u]*16,n[u]*16+16),u*16);return{data:c,count:l,fraction:o}})}var ti={light:{minAlpha:.004,minScale:1e-4,keepFraction:1},balanced:{minAlpha:.02,minScale:5e-4,keepFraction:.7},aggressive:{minAlpha:.05,minScale:.001,keepFraction:.4},mobile:{minAlpha:.05,minScale:.001,keepFraction:.25}};async function Ki(a={}){let{onLoad:t,onError:e,src:s,parts:i,quality:r="auto",...n}=a,o=r==="auto"?It():r,l=Vt(o),c=n.maxPixelRatio??l.maxPixelRatio,u=Math.min(n.shDegree??0,l.shDegreeCap),m=n.prefetchVariants??l.prefetchVariants,f=new bt({...n,maxPixelRatio:c,shDegree:u,prefetchVariants:m,tier:o}),d={destroy(){},setBackground(){},setSplatScale(){},setGamma(){},setAaDilation(){},setAutoRotate(){},setFlipY(){},resetCamera(){},focusCamera(){},getVariants(){return[]},async setVariant(){return!1},getStats(){return null},getSplatDebug(){return null},camera:null,setDebugIndex(){},async readDebug(){return null}};try{await f.init(),i?await f.loadParts(await dt(i,l.lod)):s&&await f.load(await et(s,l.lod))}catch(h){if(f.destroy(),e)return e(h),d;throw h}return f.start(),t?.(),{destroy(){f.destroy()},setBackground(h){f.setBackground(h)},setSplatScale(h){f.setSplatScale(h)},setGamma(h){f.setGamma(h)},setAaDilation(h){f.setAaDilation(h)},setAutoRotate(h){f.setAutoRotate(h)},setFlipY(h){f.setFlipY(h)},resetCamera(){f.resetCamera()},focusCamera(){f.focusCamera()},getVariants(h){return f.getVariants(h)},getStats(){return f.getStats()},getSplatDebug(h){return f.getSplatDebug(h)},setDebugIndex(h){f.setDebugIndex(h)},readDebug(){return f.readDebug()},setVariant(h,p){return f.setVariant(h,p)},get camera(){return f.camera}}}export{Ht as Animation,ti as PRUNE_PRESETS,bt as Viewer,Ms as compressToSpz,Fs as compressVariantsToSpzv,Ki as create,It as detectDeviceTier,qe as encodeSpz,je as encodeSpzv,Js as generateLods,he as loadAnimation,De as parsePly,Te as parseSplat,ie as parseSpz,Ss as parseSpzGzip,re as parseSpzv,zs as parseSpzvGzip,es as player,Ks as pruneGaussians,Vt as qualityForTier,et as resolveLodUrl,dt as resolvePartsLod,rs as scrollScene,oe as splatNameFromId};
|
|
766
|
+
//# sourceMappingURL=holosplat.esm.js.map
|