@voluma/vlam 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/budget-governor.d.ts +4 -4
- package/dist/camera-budget-governor.d.ts +11 -11
- package/dist/chunk-cache-budget.d.ts +5 -5
- package/dist/chunk-fetch-scheduler.d.ts +4 -4
- package/dist/chunk-loader-DlNwggfG.js +1859 -0
- package/dist/chunk-loader-DlNwggfG.js.map +1 -0
- package/dist/chunk-loader.d.ts +2 -2
- package/dist/compute-sorter.d.ts +1 -1
- package/dist/effects.d.ts +11 -10
- package/dist/effects.js.map +1 -1
- package/dist/formats/ksplat/index.d.ts +1 -1
- package/dist/formats/lcc/collision-mesh.d.ts +2 -2
- package/dist/formats/lcc.js +17 -16
- package/dist/formats/lcc.js.map +1 -1
- package/dist/formats/ply/index.d.ts +1 -1
- package/dist/formats/ply/parse-splat-ply.d.ts +2 -2
- package/dist/formats/ply.js +1 -1
- package/dist/formats/ply.js.map +1 -1
- package/dist/formats/rad/index.d.ts +1 -1
- package/dist/formats/rad/parse-rad.d.ts +1 -1
- package/dist/formats/rad/rad.d.ts +2 -2
- package/dist/formats/rad.js +3 -3
- package/dist/formats/rad.js.map +1 -1
- package/dist/formats/sog/index.d.ts +1 -1
- package/dist/formats/sog/parse-sog.d.ts +4 -4
- package/dist/formats/sog.js.map +1 -1
- package/dist/formats/splat/index.d.ts +1 -1
- package/dist/formats/spz/index.d.ts +1 -1
- package/dist/index.d.ts +16 -25
- package/dist/index.js +253 -9047
- package/dist/index.js.map +1 -1
- package/dist/{load-scene.d.ts → load-splat-data.d.ts} +15 -13
- package/dist/loaders.d.ts +15 -0
- package/dist/loaders.js +26 -0
- package/dist/loaders.js.map +1 -0
- package/dist/loading-wLo8vRbA.js.map +1 -1
- package/dist/loading.d.ts +8 -8
- package/dist/{splat-mesh-types-CTN_duw2.js → lod-scheduler-B0a_uBlv.js} +133 -140
- package/dist/lod-scheduler-B0a_uBlv.js.map +1 -0
- package/dist/lod-scheduler.d.ts +1 -1
- package/dist/lod-source.d.ts +5 -5
- package/dist/{splat-scene.d.ts → merged-splat-mesh.d.ts} +14 -15
- package/dist/orientation.d.ts +7 -7
- package/dist/{ply-header-BwgExCn-.js → ply-header-B9WllbPg.js} +3 -3
- package/dist/{ply-header-BwgExCn-.js.map → ply-header-B9WllbPg.js.map} +1 -1
- package/dist/radix-sorter-R0FCSqKu.js +178 -0
- package/dist/radix-sorter-R0FCSqKu.js.map +1 -0
- package/dist/relighting-CSQF3UcL.js +969 -0
- package/dist/relighting-CSQF3UcL.js.map +1 -0
- package/dist/relighting.d.ts +1 -1
- package/dist/selection.d.ts +13 -0
- package/dist/selection.js +137 -0
- package/dist/selection.js.map +1 -0
- package/dist/source-transform.d.ts +4 -4
- package/dist/{splat-budget-DXew_86c.js → splat-budget-DuVZspPQ.js} +7 -7
- package/dist/{splat-budget-DXew_86c.js.map → splat-budget-DuVZspPQ.js.map} +1 -1
- package/dist/splat-budget.d.ts +1 -1
- package/dist/splat-data-BZM5dhhx.js.map +1 -1
- package/dist/splat-data.d.ts +4 -4
- package/dist/splat-mesh-aQdcWA6v.js +1942 -0
- package/dist/splat-mesh-aQdcWA6v.js.map +1 -0
- package/dist/splat-mesh-material.d.ts +2 -2
- package/dist/splat-mesh-types-BZIko-_9.js +18 -0
- package/dist/splat-mesh-types-BZIko-_9.js.map +1 -0
- package/dist/splat-mesh-types.d.ts +14 -8
- package/dist/splat-mesh.d.ts +4 -4
- package/dist/splat-modifier.d.ts +5 -5
- package/dist/splat-partition.d.ts +1 -1
- package/dist/static-lod-entry.d.ts +12 -0
- package/dist/static-lod-splat-mesh.d.ts +2 -2
- package/dist/{static-lod-worker-D2fyqQZO.js → static-lod-worker-DktKnJkR.js} +46 -46
- package/dist/{static-lod-worker-D2fyqQZO.js.map → static-lod-worker-DktKnJkR.js.map} +1 -1
- package/dist/static-lod.js +148 -0
- package/dist/static-lod.js.map +1 -0
- package/dist/streamed-splat-mesh.d.ts +21 -27
- package/dist/streaming.d.ts +23 -0
- package/dist/streaming.js +2877 -0
- package/dist/streaming.js.map +1 -0
- package/dist/{unified-splat-renderer.d.ts → unified-splat-mesh.d.ts} +10 -10
- package/dist/unified-work-buffer.d.ts +66 -0
- package/dist/unified.d.ts +11 -0
- package/dist/unified.js +738 -0
- package/dist/unified.js.map +1 -0
- package/dist/webgpu-limits-Y3CI_6_6.js +43 -0
- package/dist/webgpu-limits-Y3CI_6_6.js.map +1 -0
- package/dist/webgpu-limits.d.ts +0 -59
- package/dist/work-buffer-gather.d.ts +1 -1
- package/package.json +24 -4
- package/dist/splat-mesh-types-CTN_duw2.js.map +0 -1
|
@@ -0,0 +1,1942 @@
|
|
|
1
|
+
var pt = Object.defineProperty;
|
|
2
|
+
var ft = (g, u, e) => u in g ? pt(g, u, { enumerable: !0, configurable: !0, writable: !0, value: e }) : g[u] = e;
|
|
3
|
+
var o = (g, u, e) => ft(g, typeof u != "symbol" ? u + "" : u, e);
|
|
4
|
+
import { r as Y, M as gt, a as mt } from "./splat-mesh-types-BZIko-_9.js";
|
|
5
|
+
import * as d from "three/webgpu";
|
|
6
|
+
import { uniform as b } from "three/tsl";
|
|
7
|
+
import { z as wt, e as xt, m as St, b as vt, D as yt, c as Rt, A as Z, B as bt, E as Tt, W as At, F as K, G as Q, H as J, I as Mt, i as It, j as kt, y as Pt, n as Ct, r as Ut, J as z, C as tt, f as Dt } from "./relighting-CSQF3UcL.js";
|
|
8
|
+
import { l as et, w as O } from "./logging-BfPdd7NJ.js";
|
|
9
|
+
import { d as Bt } from "./splat-budget-DuVZspPQ.js";
|
|
10
|
+
import { e as G } from "./half-float-Cs4FDq8Q.js";
|
|
11
|
+
import { a as Et, r as Wt, n as Lt } from "./sh-pack-D5wAe5gg.js";
|
|
12
|
+
function Ft() {
|
|
13
|
+
return new d.Matrix4().makeRotationX(Math.PI);
|
|
14
|
+
}
|
|
15
|
+
function Vt(g) {
|
|
16
|
+
switch (g) {
|
|
17
|
+
case "ply":
|
|
18
|
+
case "splat":
|
|
19
|
+
case "ksplat":
|
|
20
|
+
case "sog":
|
|
21
|
+
case "rad":
|
|
22
|
+
case "streamed-sog":
|
|
23
|
+
return Ft();
|
|
24
|
+
case "spz":
|
|
25
|
+
case "lcc":
|
|
26
|
+
case "lcc2":
|
|
27
|
+
case void 0:
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const lt = `let w = new Float32Array(0), x = new Float32Array(0);
|
|
32
|
+
const f = new Uint32Array(65536);
|
|
33
|
+
let E = 0, S = new Uint32Array(0), M = new Float32Array(0), I = new Uint32Array(0), R = new Uint32Array(0), Y = new Uint32Array(0);
|
|
34
|
+
function j(e) {
|
|
35
|
+
e <= E || (E = e, S = new Uint32Array(e), M = new Float32Array(e), I = new Uint32Array(e), R = new Uint32Array(e), Y = new Uint32Array(e));
|
|
36
|
+
}
|
|
37
|
+
function q(e, r, n) {
|
|
38
|
+
const D = e[2], K = e[6], _ = e[10], k = e[14];
|
|
39
|
+
let c = 0;
|
|
40
|
+
for (let t = 0; t < r.length; t += 2) c += r[t + 1];
|
|
41
|
+
j(c);
|
|
42
|
+
let d = 1 / 0, F = -1 / 0, X = 0;
|
|
43
|
+
for (let t = 0; t < r.length; t += 2) {
|
|
44
|
+
const i = r[t], o = i + r[t + 1];
|
|
45
|
+
for (let s = i; s < o; s++) {
|
|
46
|
+
const a = s * 4, h = x[s], l = n ? h * 16 : 0, u = w[a], U = w[a + 1], g = w[a + 2], B = n ? n[l] * u + n[l + 4] * U + n[l + 8] * g + n[l + 12] : u, T = n ? n[l + 1] * u + n[l + 5] * U + n[l + 9] * g + n[l + 13] : U, Z = n ? n[l + 2] * u + n[l + 6] * U + n[l + 10] * g + n[l + 14] : g, A = D * B + K * T + _ * Z + k;
|
|
47
|
+
S[X] = s, M[X] = A, X++, A < d && (d = A), A > F && (F = A);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const z = 16777215 / (F - d || 1);
|
|
51
|
+
let y = R;
|
|
52
|
+
for (let t = 0; t < c; t++)
|
|
53
|
+
I[t] = (M[t] - d) * z, y[t] = t;
|
|
54
|
+
let p = Y;
|
|
55
|
+
for (let t = 0; t < 24; t += 16) {
|
|
56
|
+
f.fill(0);
|
|
57
|
+
for (let o = 0; o < c; o++) {
|
|
58
|
+
const s = I[y[o]] >>> t & 65535;
|
|
59
|
+
f[s] = f[s] + 1;
|
|
60
|
+
}
|
|
61
|
+
let i = 0;
|
|
62
|
+
for (let o = 0; o < 65536; o++) {
|
|
63
|
+
const s = f[o];
|
|
64
|
+
f[o] = i, i += s;
|
|
65
|
+
}
|
|
66
|
+
for (let o = 0; o < c; o++) {
|
|
67
|
+
const s = y[o], a = I[s] >>> t & 65535, h = f[a];
|
|
68
|
+
p[h] = s, f[a] = h + 1;
|
|
69
|
+
}
|
|
70
|
+
[y, p] = [p, y];
|
|
71
|
+
}
|
|
72
|
+
R = y, Y = p;
|
|
73
|
+
const b = new Uint32Array(c);
|
|
74
|
+
for (let t = 0; t < c; t++) b[t] = S[y[t]];
|
|
75
|
+
return b;
|
|
76
|
+
}
|
|
77
|
+
self.onmessage = (e) => {
|
|
78
|
+
const r = e.data;
|
|
79
|
+
if (r.type === "init") {
|
|
80
|
+
w = new Float32Array(r.capacity * 4), x = new Float32Array(r.capacity);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (r.type === "write") {
|
|
84
|
+
w.set(r.centers, r.start * 4), r.sourceIds && x.set(r.sourceIds, r.start);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const n = q(r.modelView, r.spans, r.matrices), D = { type: "order", order: n };
|
|
88
|
+
self.postMessage(D, [n.buffer]);
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=sort-worker-CDRn-GLW.js.map
|
|
91
|
+
`, it = typeof self < "u" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", lt], { type: "text/javascript;charset=utf-8" });
|
|
92
|
+
function _t(g) {
|
|
93
|
+
let u;
|
|
94
|
+
try {
|
|
95
|
+
if (u = it && (self.URL || self.webkitURL).createObjectURL(it), !u) throw "";
|
|
96
|
+
const e = new Worker(u, {
|
|
97
|
+
type: "module",
|
|
98
|
+
name: g == null ? void 0 : g.name
|
|
99
|
+
});
|
|
100
|
+
return e.addEventListener("error", () => {
|
|
101
|
+
(self.URL || self.webkitURL).revokeObjectURL(u);
|
|
102
|
+
}), e;
|
|
103
|
+
} catch {
|
|
104
|
+
return new Worker(
|
|
105
|
+
"data:text/javascript;charset=utf-8," + encodeURIComponent(lt),
|
|
106
|
+
{
|
|
107
|
+
type: "module",
|
|
108
|
+
name: g == null ? void 0 : g.name
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
class qt {
|
|
114
|
+
constructor(u) {
|
|
115
|
+
o(this, "kind", "worker");
|
|
116
|
+
o(this, "worker");
|
|
117
|
+
o(this, "splatIndexAttribute");
|
|
118
|
+
o(this, "host");
|
|
119
|
+
o(this, "inFlight", !1);
|
|
120
|
+
/** Set by {@link dispose}; drops any already-delivered order message. */
|
|
121
|
+
o(this, "disposed", !1);
|
|
122
|
+
/** The active spans the in-flight sort was computed against. */
|
|
123
|
+
o(this, "sentSpans", null);
|
|
124
|
+
o(this, "submittedCount", 0);
|
|
125
|
+
o(this, "completedCount", 0);
|
|
126
|
+
o(this, "lastSubmittedAt", -1 / 0);
|
|
127
|
+
o(this, "lastCompletedAt", -1 / 0);
|
|
128
|
+
o(this, "lastLatencyMs", Number.NaN);
|
|
129
|
+
this.host = u, this.splatIndexAttribute = u.splatIndexAttribute, this.worker = new _t(), this.worker.onmessage = (t) => {
|
|
130
|
+
this.applyOrder(t.data.order);
|
|
131
|
+
}, this.worker.onerror = (t) => {
|
|
132
|
+
et("sort worker error - retrying on a later frame.", t.message), this.inFlight = !1;
|
|
133
|
+
}, this.worker.onmessageerror = () => {
|
|
134
|
+
et("sort worker message deserialization failed."), this.inFlight = !1;
|
|
135
|
+
};
|
|
136
|
+
const e = { type: "init", capacity: u.capacity };
|
|
137
|
+
this.worker.postMessage(e), this.pushCenters([{ start: 0, count: Math.ceil(u.capacity / u.rowWidth) }]);
|
|
138
|
+
}
|
|
139
|
+
sort(u, e, t) {
|
|
140
|
+
if (this.disposed || this.inFlight) return !1;
|
|
141
|
+
this.inFlight = !0, this.submittedCount++, this.lastSubmittedAt = performance.now(), this.pushCenters(this.host.takeDirtyRows());
|
|
142
|
+
const i = this.host.getActiveSpans();
|
|
143
|
+
this.sentSpans = i;
|
|
144
|
+
const s = {
|
|
145
|
+
type: "sort",
|
|
146
|
+
modelView: new Float32Array(u.elements),
|
|
147
|
+
spans: i,
|
|
148
|
+
matrices: this.host.perSource ? new Float32Array(this.host.perSource.matrices) : void 0
|
|
149
|
+
};
|
|
150
|
+
return this.worker.postMessage(s), !0;
|
|
151
|
+
}
|
|
152
|
+
/** Internal timing diagnostics used by the demo's opt-in XR A/B harness. */
|
|
153
|
+
snapshot() {
|
|
154
|
+
return {
|
|
155
|
+
submittedCount: this.submittedCount,
|
|
156
|
+
completedCount: this.completedCount,
|
|
157
|
+
lastSubmittedAt: this.lastSubmittedAt,
|
|
158
|
+
lastCompletedAt: this.lastCompletedAt,
|
|
159
|
+
lastLatencyMs: this.lastLatencyMs
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
dispose() {
|
|
163
|
+
this.disposed || (this.disposed = !0, this.worker.terminate());
|
|
164
|
+
}
|
|
165
|
+
/** Sends written pool rows to keep the worker's centers mirror current. */
|
|
166
|
+
pushCenters(u) {
|
|
167
|
+
var t;
|
|
168
|
+
const e = this.host.rowWidth;
|
|
169
|
+
for (const i of u) {
|
|
170
|
+
const s = this.host.centers.slice(
|
|
171
|
+
i.start * e * 4,
|
|
172
|
+
(i.start + i.count) * e * 4
|
|
173
|
+
), r = i.start * e, n = i.count * e, a = (t = this.host.perSource) == null ? void 0 : t.sourceIds.slice(r, r + n), c = { type: "write", start: r, centers: s, sourceIds: a };
|
|
174
|
+
this.worker.postMessage(c, [s.buffer]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
applyOrder(u) {
|
|
178
|
+
var s, r;
|
|
179
|
+
if (this.inFlight = !1, this.completedCount++, this.lastCompletedAt = performance.now(), this.lastLatencyMs = this.lastCompletedAt - this.lastSubmittedAt, this.disposed) return;
|
|
180
|
+
const e = this.host.getActiveSpans(), t = this.sentSpans;
|
|
181
|
+
if (this.sentSpans = null, !t || t.length !== e.length) return;
|
|
182
|
+
for (let n = 0; n < t.length; n++)
|
|
183
|
+
if (t[n] !== e[n]) return;
|
|
184
|
+
const i = this.splatIndexAttribute.array;
|
|
185
|
+
for (let n = 0; n < u.length; n++)
|
|
186
|
+
i[n] = u[n];
|
|
187
|
+
this.splatIndexAttribute.addUpdateRange(0, u.length), this.splatIndexAttribute.needsUpdate = !0, (r = (s = this.host).onOrderApplied) == null || r.call(s);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const zt = (1 << 24) - 1, st = new d.Vector2(), V = new d.Raycaster(), rt = new d.Vector3();
|
|
191
|
+
function Ot(g, u, e) {
|
|
192
|
+
return ((g & 255) << 16 | (u & 255) << 8 | e & 255) / zt;
|
|
193
|
+
}
|
|
194
|
+
function Gt(g, u, e) {
|
|
195
|
+
return u + g * (e - u);
|
|
196
|
+
}
|
|
197
|
+
function Ht(g, u, e, t, i) {
|
|
198
|
+
st.set(g, u), V.setFromCamera(st, t), rt.copy(V.ray.direction).transformDirection(t.matrixWorldInverse);
|
|
199
|
+
const s = e / -rt.z;
|
|
200
|
+
return i.copy(V.ray.origin).addScaledVector(V.ray.direction, s), { point: i, distance: i.distanceTo(V.ray.origin) };
|
|
201
|
+
}
|
|
202
|
+
class Nt {
|
|
203
|
+
constructor(u) {
|
|
204
|
+
/** Alpha below which a splat is transparent enough to pick through. */
|
|
205
|
+
o(this, "alphaThreshold", b(0.1));
|
|
206
|
+
/** Camera planes for the pick material's depth encoding. */
|
|
207
|
+
o(this, "near", b(0.1));
|
|
208
|
+
o(this, "far", b(1e3));
|
|
209
|
+
o(this, "material", null);
|
|
210
|
+
o(this, "proxy", null);
|
|
211
|
+
o(this, "target", null);
|
|
212
|
+
/** Reused sub-frustum camera; preserves the caller's concrete camera type. */
|
|
213
|
+
o(this, "pickCamera", null);
|
|
214
|
+
o(this, "scene", new d.Scene());
|
|
215
|
+
/** Serializes picks: they share one render target and one renderer. */
|
|
216
|
+
o(this, "queue", Promise.resolve());
|
|
217
|
+
o(this, "point", new d.Vector3());
|
|
218
|
+
o(this, "savedClearColor", new d.Color());
|
|
219
|
+
this.host = u;
|
|
220
|
+
}
|
|
221
|
+
/** The pick uniforms the material graph binds. */
|
|
222
|
+
get uniforms() {
|
|
223
|
+
return { alphaThreshold: this.alphaThreshold, near: this.near, far: this.far };
|
|
224
|
+
}
|
|
225
|
+
pick(u, e, t, i) {
|
|
226
|
+
const s = this.queue.then(() => this.run(u, e, t, i));
|
|
227
|
+
return this.queue = s.then(
|
|
228
|
+
() => {
|
|
229
|
+
},
|
|
230
|
+
() => {
|
|
231
|
+
}
|
|
232
|
+
), s;
|
|
233
|
+
}
|
|
234
|
+
/** Rebuilds the pick graph after a settings change. No-op before the first pick. */
|
|
235
|
+
rebuildMaterial() {
|
|
236
|
+
this.material && this.host.applyPickGraph(this.material);
|
|
237
|
+
}
|
|
238
|
+
/** Flags the pick graph for recompile (e.g. the modifier list changed). */
|
|
239
|
+
markNeedsUpdate() {
|
|
240
|
+
this.material && (this.material.needsUpdate = !0);
|
|
241
|
+
}
|
|
242
|
+
dispose() {
|
|
243
|
+
var u, e;
|
|
244
|
+
(u = this.material) == null || u.dispose(), this.material = null, this.scene.clear(), this.proxy = null, (e = this.target) == null || e.dispose(), this.target = null, this.pickCamera = null;
|
|
245
|
+
}
|
|
246
|
+
async run(u, e, t, i) {
|
|
247
|
+
if (this.host.isDisposed() || this.host.getActiveCount() === 0 || u.x < -1 || u.x > 1 || u.y < -1 || u.y > 1) return null;
|
|
248
|
+
if (wt(e))
|
|
249
|
+
throw new Error(
|
|
250
|
+
"SplatMesh.pick: an XR array camera has no single frustum to pick through. Pass one eye (renderer.xr.getCamera().cameras[i]) or a mono camera, and give `ndc` in that eye’s viewport."
|
|
251
|
+
);
|
|
252
|
+
e.updateMatrixWorld(!0), this.host.updateWorldMatrix(), this.host.prepare(e, t);
|
|
253
|
+
const s = Math.max(1, Math.floor(this.host.getViewportSize().x)), r = Math.max(1, Math.floor(this.host.getViewportSize().y)), n = Math.floor((u.x * 0.5 + 0.5) * s), a = Math.floor((u.y * 0.5 + 0.5) * r);
|
|
254
|
+
if (n < 0 || a < 0 || n >= s || a >= r) return null;
|
|
255
|
+
this.ensureResources(e);
|
|
256
|
+
const c = this.target, h = this.proxy, l = this.pickCamera;
|
|
257
|
+
this.cropCameraToPixel(l, e, s, r, n, a), this.host.setView(l, 1, 1);
|
|
258
|
+
const f = "near" in e && typeof e.near == "number" ? e.near : 0.1, x = "far" in e && typeof e.far == "number" ? e.far : 1e3;
|
|
259
|
+
this.near.value = f, this.far.value = x, this.alphaThreshold.value = (i == null ? void 0 : i.alphaThreshold) !== void 0 ? i.alphaThreshold : 0.1;
|
|
260
|
+
const p = this.host.mesh, w = t.getRenderTarget(), m = t.getScissorTest();
|
|
261
|
+
t.getClearColor(this.savedClearColor);
|
|
262
|
+
const S = t.getClearAlpha(), A = t.autoClear;
|
|
263
|
+
h.matrix.copy(p.matrixWorld), h.matrixWorld.copy(p.matrixWorld), h.visible = this.host.getPickVisible(), h.layers.mask = p.layers.mask, h.renderOrder = p.renderOrder, c.viewport.set(0, 0, 1, 1), c.scissor.set(0, 0, 1, 1);
|
|
264
|
+
const R = (() => {
|
|
265
|
+
try {
|
|
266
|
+
return t.setRenderTarget(c), t.setScissorTest(!1), t.setClearColor(0, 0), t.autoClear = !0, t.clear(), t.setScissorTest(!0), t.autoClear = !1, t.render(this.scene, l), t.readRenderTargetPixelsAsync(c, 0, 0, 1, 1);
|
|
267
|
+
} finally {
|
|
268
|
+
this.host.setView(e, s, r), t.setRenderTarget(w), t.setScissorTest(m), t.setClearColor(this.savedClearColor, S), t.autoClear = A;
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
let y;
|
|
272
|
+
try {
|
|
273
|
+
y = await R;
|
|
274
|
+
} catch (L) {
|
|
275
|
+
if (this.host.isDisposed()) return null;
|
|
276
|
+
throw L;
|
|
277
|
+
}
|
|
278
|
+
if (this.host.isDisposed()) return null;
|
|
279
|
+
const M = y[0], U = y[1], I = y[2];
|
|
280
|
+
if (y[3] === 0) return null;
|
|
281
|
+
const B = Gt(Ot(M, U, I), f, x), { point: E, distance: W } = Ht(u.x, u.y, B, e, this.point);
|
|
282
|
+
return { point: E.clone(), distance: W };
|
|
283
|
+
}
|
|
284
|
+
ensureResources(u) {
|
|
285
|
+
this.target === null && (this.target = new d.RenderTarget(1, 1, {
|
|
286
|
+
format: d.RGBAFormat,
|
|
287
|
+
type: d.UnsignedByteType,
|
|
288
|
+
depthBuffer: !0,
|
|
289
|
+
stencilBuffer: !1
|
|
290
|
+
})), this.material === null && (this.material = new d.NodeMaterial(), this.host.applyPickGraph(this.material), this.proxy = new d.Mesh(this.host.mesh.geometry, this.material), this.proxy.matrixAutoUpdate = !1, this.proxy.frustumCulled = !1, this.scene.add(this.proxy)), (this.pickCamera === null || this.pickCamera.constructor !== u.constructor) && (this.pickCamera = u.clone());
|
|
291
|
+
}
|
|
292
|
+
/** Maps one original framebuffer pixel onto the complete 1×1 pick target. */
|
|
293
|
+
cropCameraToPixel(u, e, t, i, s, r) {
|
|
294
|
+
u.copy(e);
|
|
295
|
+
const n = u.projectionMatrix, a = n.elements, c = 2 * s + 1 - t, h = 2 * r + 1 - i;
|
|
296
|
+
for (let l = 0; l < 4; l++) {
|
|
297
|
+
const f = a[l * 4 + 3];
|
|
298
|
+
a[l * 4] = a[l * 4] * t - c * f, a[l * 4 + 1] = a[l * 4 + 1] * i - h * f;
|
|
299
|
+
}
|
|
300
|
+
u.projectionMatrixInverse.copy(n).invert();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const Xt = 1024, jt = 1 << 21;
|
|
304
|
+
class $t {
|
|
305
|
+
/**
|
|
306
|
+
* @param centers - The pool's CPU centers (stride 4: x,y,z,_ per splat).
|
|
307
|
+
* @param poolIndices - The pool indices to index (e.g. the active list).
|
|
308
|
+
* @param count - How many entries of `poolIndices` are valid.
|
|
309
|
+
*/
|
|
310
|
+
constructor(u, e, t) {
|
|
311
|
+
/** Populated-region minimum corner (local space). */
|
|
312
|
+
o(this, "min", new Float64Array(3));
|
|
313
|
+
/** Per-axis cell size (local units). */
|
|
314
|
+
o(this, "cellSize", new Float64Array(3));
|
|
315
|
+
/** Per-axis cell counts. */
|
|
316
|
+
o(this, "dims", new Int32Array(3));
|
|
317
|
+
/** CSR cell offsets: cell c owns `items[start[c] .. start[c+1])`. */
|
|
318
|
+
o(this, "cellStart");
|
|
319
|
+
/** Pool indices, bucketed by cell. */
|
|
320
|
+
o(this, "items");
|
|
321
|
+
/** Pool centers array this grid points into (RGBA stride 4). */
|
|
322
|
+
o(this, "centers");
|
|
323
|
+
o(this, "count");
|
|
324
|
+
if (this.centers = u, this.count = t, t === 0) {
|
|
325
|
+
this.dims.set([1, 1, 1]), this.cellSize.set([1, 1, 1]), this.cellStart = new Int32Array(2), this.items = new Uint32Array(0);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const i = [1 / 0, 1 / 0, 1 / 0], s = [-1 / 0, -1 / 0, -1 / 0];
|
|
329
|
+
for (let p = 0; p < t; p++) {
|
|
330
|
+
const w = e[p] * 4;
|
|
331
|
+
for (let m = 0; m < 3; m++) {
|
|
332
|
+
const S = u[w + m];
|
|
333
|
+
S < i[m] && (i[m] = S), S > s[m] && (s[m] = S);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const r = [
|
|
337
|
+
s[0] - i[0],
|
|
338
|
+
s[1] - i[1],
|
|
339
|
+
s[2] - i[2]
|
|
340
|
+
], n = r.filter((p) => p > 0), a = n.reduce((p, w) => p * w, 1);
|
|
341
|
+
let c = n.length > 0 ? Math.pow(a / t, 1 / n.length) : 1;
|
|
342
|
+
(!(c > 0) || !Number.isFinite(c)) && (c = 1);
|
|
343
|
+
const h = (p) => {
|
|
344
|
+
for (let w = 0; w < 3; w++) {
|
|
345
|
+
const m = r[w];
|
|
346
|
+
this.dims[w] = m > 0 ? Math.max(1, Math.min(Xt, Math.round(m / p) || 1)) : 1, this.cellSize[w] = this.dims[w] > 0 && m / this.dims[w] || 1, this.min[w] = i[w];
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
for (h(c); this.dims[0] * this.dims[1] * this.dims[2] > jt; )
|
|
350
|
+
c *= 1.5, h(c);
|
|
351
|
+
const l = this.dims[0] * this.dims[1] * this.dims[2], f = new Int32Array(l + 1);
|
|
352
|
+
for (let p = 0; p < t; p++) f[this.cellOf(e[p]) + 1]++;
|
|
353
|
+
for (let p = 0; p < l; p++) f[p + 1] += f[p];
|
|
354
|
+
this.cellStart = f, this.items = new Uint32Array(t);
|
|
355
|
+
const x = Int32Array.from(f.subarray(0, l));
|
|
356
|
+
for (let p = 0; p < t; p++) {
|
|
357
|
+
const w = e[p];
|
|
358
|
+
this.items[x[this.cellOf(w)]++] = w;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** The cell index of pool splat `p`, clamped into range. */
|
|
362
|
+
cellOf(u) {
|
|
363
|
+
const e = u * 4, t = this.axisCell(0, this.centers[e]), i = this.axisCell(1, this.centers[e + 1]);
|
|
364
|
+
return (this.axisCell(2, this.centers[e + 2]) * this.dims[1] + i) * this.dims[0] + t;
|
|
365
|
+
}
|
|
366
|
+
axisCell(u, e) {
|
|
367
|
+
const t = Math.floor((e - this.min[u]) / this.cellSize[u]);
|
|
368
|
+
return Math.max(0, Math.min(this.dims[u] - 1, t));
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Visits every indexed splat within `radius` (local units) of the local
|
|
372
|
+
* point, calling `visit(poolIndex, distanceSq)`. Cells are pruned by their
|
|
373
|
+
* axis span and each candidate is distance-filtered here, so the callback
|
|
374
|
+
* only ever sees splats truly within the radius.
|
|
375
|
+
*/
|
|
376
|
+
forEachWithin(u, e, t, i, s) {
|
|
377
|
+
if (this.count === 0 || i < 0) return;
|
|
378
|
+
const r = i * i, n = [0, 0, 0], a = [0, 0, 0], c = [u, e, t];
|
|
379
|
+
for (let h = 0; h < 3; h++) {
|
|
380
|
+
const l = Math.ceil(i / this.cellSize[h]), f = this.axisCell(h, c[h]);
|
|
381
|
+
n[h] = Math.max(0, f - l), a[h] = Math.min(this.dims[h] - 1, f + l);
|
|
382
|
+
}
|
|
383
|
+
for (let h = n[2]; h <= a[2]; h++)
|
|
384
|
+
for (let l = n[1]; l <= a[1]; l++) {
|
|
385
|
+
const f = (h * this.dims[1] + l) * this.dims[0], x = this.cellStart[f + n[0]], p = this.cellStart[f + a[0] + 1];
|
|
386
|
+
for (let w = x; w < p; w++) {
|
|
387
|
+
const m = this.items[w], S = m * 4, A = this.centers[S] - u, R = this.centers[S + 1] - e, y = this.centers[S + 2] - t, M = A * A + R * R + y * y;
|
|
388
|
+
M <= r && s(m, M);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The nearest indexed splat to the local point within `radius`, or null.
|
|
394
|
+
* Returns its pool index and squared local distance.
|
|
395
|
+
*/
|
|
396
|
+
nearest(u, e, t, i) {
|
|
397
|
+
let s = -1, r = 1 / 0;
|
|
398
|
+
return this.forEachWithin(u, e, t, i, (n, a) => {
|
|
399
|
+
a < r && (r = a, s = n);
|
|
400
|
+
}), s < 0 ? null : { poolIndex: s, distSq: r };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const Yt = 4, v = class v extends d.Mesh {
|
|
404
|
+
constructor(e, t = {}) {
|
|
405
|
+
var W, L;
|
|
406
|
+
const i = bt(t.sortIntervalMs), s = ((W = Bt()) == null ? void 0 : W.isMobile) === !0, r = ct(t.maxStdDev) ?? 3, n = Jt(t.minSplatSizePx) ?? (s ? 1.5 : 0), a = !("capacity" in e), c = a ? e.count : e.capacity;
|
|
407
|
+
if (c <= 0) throw new Error("SplatMesh capacity must be positive.");
|
|
408
|
+
const h = (t.shBands ?? (Y(t.performanceProfile) === "smooth" ? 0 : gt)) !== 0, l = a ? h ? ((L = e.shPacked) == null ? void 0 : L.bands) ?? 0 : 0 : t.shBands ?? 0, f = t.pool, x = () => new Dt({
|
|
409
|
+
capacity: c,
|
|
410
|
+
floatTextures: t.poolFloatTextures,
|
|
411
|
+
packedShBands: l,
|
|
412
|
+
packedShTextureCount: l === 0 ? 0 : Math.ceil(J(l) / 4),
|
|
413
|
+
...t.maxTextureSize === void 0 ? {} : { maxTextureSize: t.maxTextureSize }
|
|
414
|
+
}), p = f !== void 0 && l !== f.packedShBands;
|
|
415
|
+
p && O(
|
|
416
|
+
`a mesh with shBands ${l} cannot share a pool built for ${f == null ? void 0 : f.packedShBands}; it allocated its own pool instead.`
|
|
417
|
+
);
|
|
418
|
+
const w = f === void 0 || p, m = w ? x() : f, S = Math.min(
|
|
419
|
+
m.capacity,
|
|
420
|
+
Math.ceil(c / v.DATA_TEXTURE_WIDTH) * v.DATA_TEXTURE_WIDTH
|
|
421
|
+
), A = m.centersTexture, R = m.colorsTexture, y = m.covarianceATexture, M = m.covarianceBTexture, U = a && e.sh && h ? Tt(
|
|
422
|
+
e.sh.palette,
|
|
423
|
+
e.sh.paletteWidth,
|
|
424
|
+
e.sh.paletteHeight,
|
|
425
|
+
d.FloatType
|
|
426
|
+
) : null, I = m.shPackedTextures, D = new d.InstancedBufferGeometry();
|
|
427
|
+
D.setIndex([0, 1, 2, 0, 2, 3]), D.setAttribute(
|
|
428
|
+
"position",
|
|
429
|
+
new d.BufferAttribute(new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]), 3)
|
|
430
|
+
);
|
|
431
|
+
const B = new Float32Array(S);
|
|
432
|
+
for (let C = 0; C < S; C++) B[C] = C;
|
|
433
|
+
const E = new d.StorageInstancedBufferAttribute(B, 1);
|
|
434
|
+
D.setAttribute("splatIndex", E), D.instanceCount = 0;
|
|
435
|
+
super(D, new d.NodeMaterial());
|
|
436
|
+
/**
|
|
437
|
+
* The storage this mesh draws from. Held rather than inlined because a
|
|
438
|
+
* splat's draw identity is already independent of its pool slot (`splatIndex`
|
|
439
|
+
* indirects every instance), so the pool is a separable concern - see
|
|
440
|
+
* {@link SplatPool}.
|
|
441
|
+
*/
|
|
442
|
+
o(this, "pool");
|
|
443
|
+
/** False when the pool was supplied by the caller, and so outlives this mesh. */
|
|
444
|
+
o(this, "ownsPool");
|
|
445
|
+
o(this, "splatIndexAttribute");
|
|
446
|
+
o(this, "sourceIndexAttribute");
|
|
447
|
+
o(this, "dataTextures");
|
|
448
|
+
/** True when constructed from a complete SplatData (single fixed range). */
|
|
449
|
+
o(this, "isStatic");
|
|
450
|
+
/** Constructor-created range, retained for static in-place LOD replacement. */
|
|
451
|
+
o(this, "staticRange");
|
|
452
|
+
o(this, "shEnabled");
|
|
453
|
+
/**
|
|
454
|
+
* Dequantization range for the packed SH, shared by every coefficient.
|
|
455
|
+
* Uniforms rather than constants: the first chunk to arrive supplies it,
|
|
456
|
+
* long after the material is built.
|
|
457
|
+
*/
|
|
458
|
+
o(this, "shRange");
|
|
459
|
+
/** Set once the first packed-SH chunk has supplied {@link shRange}. */
|
|
460
|
+
o(this, "shRangeSet", !1);
|
|
461
|
+
/** Whether SH-less rows were neutral-filled before the scene range locked. */
|
|
462
|
+
o(this, "wrotePreLockNeutralSh", !1);
|
|
463
|
+
o(this, "ranges", /* @__PURE__ */ new Map());
|
|
464
|
+
o(this, "activeCount", 0);
|
|
465
|
+
/**
|
|
466
|
+
* Bumped whenever the resident splat set or its positions change (activate,
|
|
467
|
+
* deactivate, compact) - the signal the lazy spatial-query grid rebuilds on.
|
|
468
|
+
* A depth re-sort reorders the active list but changes neither, so it does
|
|
469
|
+
* not bump this. See {@link queryNearest}.
|
|
470
|
+
*/
|
|
471
|
+
o(this, "queryEpoch", 0);
|
|
472
|
+
o(this, "queryGrid", null);
|
|
473
|
+
o(this, "queryGridEpoch", -1);
|
|
474
|
+
/**
|
|
475
|
+
* True when the shared depth-order buffer holds a *secondary* view's order
|
|
476
|
+
* (left by {@link renderView}), so the next primary {@link update} must
|
|
477
|
+
* re-sort even if its camera has not moved. See {@link renderView} (M10).
|
|
478
|
+
*/
|
|
479
|
+
o(this, "orderIsForeign", !1);
|
|
480
|
+
/**
|
|
481
|
+
* True while the draw list holds a CPU-worker depth permutation rather than
|
|
482
|
+
* the identity active order. Active-list mutations must then resync the
|
|
483
|
+
* whole drawn prefix instead of patching slots: patching a *permutation*
|
|
484
|
+
* with identity active-list values creates duplicates and drops live splats
|
|
485
|
+
* for every frame until the worker's next order lands (the WebGL2 streamed
|
|
486
|
+
* flicker, ROADMAP L5).
|
|
487
|
+
*/
|
|
488
|
+
o(this, "drawListSorted", !1);
|
|
489
|
+
/** CPU timings from the most recent render-preparation update (reset in place per frame). */
|
|
490
|
+
o(this, "updateTimings", {
|
|
491
|
+
activeListMs: 0,
|
|
492
|
+
uploadMs: 0,
|
|
493
|
+
sortSubmitMs: 0,
|
|
494
|
+
stagingTextureAllocations: 0,
|
|
495
|
+
activeListUpdateRanges: 0
|
|
496
|
+
});
|
|
497
|
+
// Protected so a unified {@link MergedSplatMesh} subclass can substitute a
|
|
498
|
+
// world-space sort bound (see {@link refreshSortBounds}).
|
|
499
|
+
o(this, "localBounds", new d.Box3().makeEmpty());
|
|
500
|
+
o(this, "boundingSphereLocal", new d.Sphere());
|
|
501
|
+
o(this, "boundsDirty", !0);
|
|
502
|
+
/** Cached {@link getUnifiedSourceView} result; see the invalidation checks there. */
|
|
503
|
+
o(this, "cachedUnifiedView", null);
|
|
504
|
+
o(this, "cachedUnifiedViewMatrixWorld", new d.Matrix4().makeScale(0, 0, 0));
|
|
505
|
+
o(this, "cachedUnifiedViewLocalBounds", new d.Sphere());
|
|
506
|
+
o(this, "cachedUnifiedViewWorldBounds", new d.Sphere());
|
|
507
|
+
/** Row spans written since the last flush, awaiting GPU upload. */
|
|
508
|
+
o(this, "pendingUploadRows", []);
|
|
509
|
+
/** Row spans awaiting mirroring to the CPU sort worker (WebGL2 path). */
|
|
510
|
+
o(this, "workerDirtyRows", []);
|
|
511
|
+
/** Persistent copy sources avoid allocating four temporary textures per upload region. */
|
|
512
|
+
o(this, "uploadStaging", /* @__PURE__ */ new Map());
|
|
513
|
+
/** Reusable float→half encode buffers keyed like {@link uploadStaging}. */
|
|
514
|
+
o(this, "halfEncodeBuffers", /* @__PURE__ */ new Map());
|
|
515
|
+
/** Opt-in per-splat channels by name; see {@link defineChannel}. */
|
|
516
|
+
o(this, "channels", /* @__PURE__ */ new Map());
|
|
517
|
+
/** Created on the first update, when the renderer backend is known. */
|
|
518
|
+
o(this, "sorter", null);
|
|
519
|
+
/**
|
|
520
|
+
* Set by a unified {@link MergedSplatMesh} subclass before the first sort: makes
|
|
521
|
+
* the WebGPU sorter transform each splat's center to world space by its
|
|
522
|
+
* source's matrix before measuring depth. `null` for an ordinary mesh, whose
|
|
523
|
+
* sorter is byte-identical to before. See `source-transform.ts`.
|
|
524
|
+
*/
|
|
525
|
+
o(this, "perSourceSort", null);
|
|
526
|
+
/** Effect hooks folded into the vertex graph; see {@link modifiers}. */
|
|
527
|
+
o(this, "modifierList", []);
|
|
528
|
+
/** Increments when a unified gather must rebuild its source graph. */
|
|
529
|
+
o(this, "graphRevision", 0);
|
|
530
|
+
/** Version for unified work-buffer caching; source pool writes are observable. */
|
|
531
|
+
o(this, "contentRevision", 0);
|
|
532
|
+
/** Material-graph inputs kept for rebuilds when the modifier list changes. */
|
|
533
|
+
o(this, "materialInputs");
|
|
534
|
+
/** Projection focal lengths in pixels; updated every frame. */
|
|
535
|
+
o(this, "focal", b(new d.Vector2()));
|
|
536
|
+
/** Drawing-buffer size in pixels; updated every frame. */
|
|
537
|
+
o(this, "viewport", b(new d.Vector2()));
|
|
538
|
+
/** Camera position in this mesh's local space, for SH view dependence. */
|
|
539
|
+
o(this, "localCameraPosition", b(new d.Vector3()));
|
|
540
|
+
/**
|
|
541
|
+
* Frontier-cut limit for `foveationMode: 'frontier'` - the maximum
|
|
542
|
+
* `own_size / distance` a splat may have and still draw (Spark's
|
|
543
|
+
* `pixelScaleLimit`). Set each frame to `foveationTargetPx / focalPx` so the
|
|
544
|
+
* cut targets a fixed on-screen size. Unused in `'band'` mode.
|
|
545
|
+
*/
|
|
546
|
+
o(this, "pixelScaleLimit", b(0));
|
|
547
|
+
/**
|
|
548
|
+
* Core projected-2D depth of field (live uniforms). Aperture `0` disables.
|
|
549
|
+
* Prefer this over the M13 `depthOfFieldPreset` modifier for camera DoF.
|
|
550
|
+
*/
|
|
551
|
+
o(this, "dofFocusDistance", b(10));
|
|
552
|
+
o(this, "dofAperture", b(0));
|
|
553
|
+
/**
|
|
554
|
+
* Proxy-mesh screen-space relighting (live uniforms + map). `blend === 0`
|
|
555
|
+
* disables. Swapping the map rebuilds the material graph once.
|
|
556
|
+
*/
|
|
557
|
+
o(this, "relightPlaceholder", St());
|
|
558
|
+
o(this, "relightMap", this.relightPlaceholder);
|
|
559
|
+
o(this, "relightBlend", b(0));
|
|
560
|
+
o(this, "relightBrightness", b(vt));
|
|
561
|
+
o(this, "relightBackground", b(yt));
|
|
562
|
+
o(this, "relightSoftness", b(Rt));
|
|
563
|
+
/**
|
|
564
|
+
* Live screen-radius band bounds (px), seeded from
|
|
565
|
+
* {@link SplatMeshOptions.minSplatScreenRadius} / `maxSplatScreenRadius` and
|
|
566
|
+
* moved by {@link setScreenRadiusBand} - so a foveated mesh can widen the band
|
|
567
|
+
* as its LOD cut refines rather than culling the detail the refinement bought.
|
|
568
|
+
*/
|
|
569
|
+
o(this, "screenBandMin", b(0));
|
|
570
|
+
o(this, "screenBandMax", b(0));
|
|
571
|
+
/** The pick pass and everything it allocates; see `splat-mesh-picking.ts`. */
|
|
572
|
+
o(this, "picker", new Nt({
|
|
573
|
+
mesh: this,
|
|
574
|
+
isDisposed: () => this.disposed,
|
|
575
|
+
getActiveCount: () => this.activeCount,
|
|
576
|
+
getViewportSize: () => this.viewport.value,
|
|
577
|
+
getPickVisible: () => this.effectiveVisibility,
|
|
578
|
+
hasSorter: () => this.sorter !== null,
|
|
579
|
+
updateWorldMatrix: () => this.updateWorldMatrix(!0, !1),
|
|
580
|
+
prepare: (e, t) => {
|
|
581
|
+
this.flushPendingUploads(t), this.refreshProjectionUniforms(e, t), this.sorter !== null && this.requestSortIfNeeded(e, t);
|
|
582
|
+
},
|
|
583
|
+
setView: (e, t, i) => this.writeViewUniforms(e, t, i),
|
|
584
|
+
applyPickGraph: (e) => Z(
|
|
585
|
+
e,
|
|
586
|
+
"pick",
|
|
587
|
+
this.graphInputs(this.materialInputs.textures, this.materialInputs.sh)
|
|
588
|
+
)
|
|
589
|
+
}));
|
|
590
|
+
/** Visibility supplied by an owning unified renderer for source-only picks. */
|
|
591
|
+
o(this, "unifiedPickVisibility", null);
|
|
592
|
+
/** Set by {@link dispose}; subclasses use it to drop late async results. */
|
|
593
|
+
o(this, "disposed", !1);
|
|
594
|
+
/** The renderer this mesh last updated with, so {@link dispose} can free
|
|
595
|
+
* storage-attribute GPU buffers that never sat in a geometry. */
|
|
596
|
+
o(this, "lastRenderer", null);
|
|
597
|
+
o(this, "currentModelView", new d.Matrix4());
|
|
598
|
+
/** Initialized to an impossible matrix so the first frame always sorts. */
|
|
599
|
+
o(this, "lastSortedModelView", new d.Matrix4().makeScale(0, 0, 0));
|
|
600
|
+
o(this, "sortScheduler");
|
|
601
|
+
/** One-frame queue-headroom hint used before a staged atomic commit. */
|
|
602
|
+
o(this, "deferSortRequestOnce", !1);
|
|
603
|
+
/** Bumped by every active-list mutation; identifies a draw list. */
|
|
604
|
+
o(this, "activeListVersion", 0);
|
|
605
|
+
/** The active list the current depth order was built from. */
|
|
606
|
+
o(this, "sortedActiveListVersion", -1);
|
|
607
|
+
o(this, "sortStrategy");
|
|
608
|
+
/** Resolved only when `sortStrategy === 'radix'`; see {@link ensureRadixSorter}. */
|
|
609
|
+
o(this, "RadixSorterCtor", null);
|
|
610
|
+
o(this, "radixSorterLoad", null);
|
|
611
|
+
o(this, "performanceProfileValue");
|
|
612
|
+
/** Gaussian cutoff radius in σ; baked into the material graph. */
|
|
613
|
+
o(this, "maxStdDevValue");
|
|
614
|
+
/** Screen-space minimum splat radius in px; baked into the material graph. */
|
|
615
|
+
o(this, "minSplatSizePx");
|
|
616
|
+
/** Mip-Splatting 2D antialiasing filter; baked into the material graph. */
|
|
617
|
+
o(this, "antialias");
|
|
618
|
+
/** Format-selected low-pass and opacity-compensation policy. */
|
|
619
|
+
o(this, "projectedFilterProfile");
|
|
620
|
+
/** Emit sRGB colors for gamma-space compositing; baked into the material graph. */
|
|
621
|
+
o(this, "srgbOutput");
|
|
622
|
+
/** Screen-radius cull threshold in px (0 = off); baked into the material graph. */
|
|
623
|
+
o(this, "maxSplatScreenRadius");
|
|
624
|
+
o(this, "minSplatScreenRadius");
|
|
625
|
+
/** `.rad` foveation cut selector; baked into the material graph. */
|
|
626
|
+
o(this, "foveationMode");
|
|
627
|
+
/** Finest on-screen node size (px) the frontier cut may request. */
|
|
628
|
+
o(this, "foveationTargetPx");
|
|
629
|
+
/** Target upper bound on the frontier cut's drawn-splat count. */
|
|
630
|
+
o(this, "foveationDrawBudget");
|
|
631
|
+
/** Rendered major/minor axis-ratio cap (0 = off); baked into the material graph. */
|
|
632
|
+
o(this, "maxSplatAspect");
|
|
633
|
+
/** Spark LOD alpha encoding + merged-node rendering; baked into the material. */
|
|
634
|
+
o(this, "lodAlpha");
|
|
635
|
+
/** Live frontier limit in px; self-adjusts between `foveationTargetPx` (finest)
|
|
636
|
+
* and coarser to hold `foveationDrawBudget`. Feeds `pixelScaleLimit` each frame. */
|
|
637
|
+
o(this, "foveationLimitPx");
|
|
638
|
+
/** Throttle timestamp for the adaptive-limit estimate (ms, `performance.now`). */
|
|
639
|
+
o(this, "lastFoveationAdaptAt", -1 / 0);
|
|
640
|
+
/** Y-up normalization policy; drives the static self-orientation below. */
|
|
641
|
+
o(this, "orientation");
|
|
642
|
+
/** Set once the pool has been checked against the device texture limit. */
|
|
643
|
+
o(this, "textureLimitChecked", !1);
|
|
644
|
+
if (this.pool = m, this.ownsPool = w, m.register(this), this.sortScheduler = new At(i, s), this.sortStrategy = t.sortStrategy ?? "counting", this.sortStrategy === "radix" && this.ensureRadixSorter(), this.performanceProfileValue = Y(t.performanceProfile), this.maxStdDevValue = r, this.minSplatSizePx = n, this.antialias = t.antialias ?? (a ? e.antialias ?? !1 : !1), this.projectedFilterProfile = t.projectedFilterProfile ?? "default", this.srgbOutput = t.srgbOutput ?? !1, this.maxSplatScreenRadius = H(t.maxSplatScreenRadius), this.minSplatScreenRadius = H(t.minSplatScreenRadius), this.screenBandMin.value = this.minSplatScreenRadius, this.screenBandMax.value = this.maxSplatScreenRadius, this.foveationMode = mt(t.foveationMode), this.foveationTargetPx = ee(t.foveationTargetPx), this.foveationDrawBudget = se(t.foveationDrawBudget), this.foveationLimitPx = this.foveationTargetPx, this.maxSplatAspect = H(t.maxSplatAspect), this.lodAlpha = t.lodAlpha ?? !1, this.orientation = t.orientation ?? "y-up", this.splatIndexAttribute = E, this.sourceIndexAttribute = new d.StorageBufferAttribute(new Uint32Array(S), 1), this.dataTextures = m.coreTextures, this.isStatic = a, this.shEnabled = U !== null, this.shRange = { min: K(), max: K() }, this.frustumCulled = !1, U && (this.dataTextures = [...this.dataTextures, U]), I.length > 0 && (this.dataTextures = [...this.dataTextures, ...I]), this.materialInputs = {
|
|
645
|
+
textures: { centersTexture: A, colorsTexture: R, covarianceATexture: y, covarianceBTexture: M },
|
|
646
|
+
sh: a && e.sh && U ? { mode: "palette", bands: e.sh.bands, paletteTexture: U } : l !== 0 ? {
|
|
647
|
+
mode: "packed",
|
|
648
|
+
bands: l,
|
|
649
|
+
textures: I,
|
|
650
|
+
range: this.shRange
|
|
651
|
+
} : null
|
|
652
|
+
}, this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh), a) {
|
|
653
|
+
this.staticRange = this.appendRange(e), this.pendingUploadRows = [], this.poolFloatTextures === "float16" && this.syncHalfFloatPoolImages();
|
|
654
|
+
const C = this.orientation === "y-up" ? Vt(e.format) : null;
|
|
655
|
+
C && (this.matrix.copy(C), this.matrix.decompose(this.position, this.quaternion, this.scale), this.matrixWorldNeedsUpdate = !0);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Pool index → this mesh's packed active-list slot. Lives on the pool: it is
|
|
660
|
+
* keyed by pool index, so a per-mesh copy would span the whole pool and
|
|
661
|
+
* several meshes sharing one would each pay for all of it. A row has one
|
|
662
|
+
* owner at a time, so the single array is unambiguous.
|
|
663
|
+
*/
|
|
664
|
+
get activeSlotByPoolIndex() {
|
|
665
|
+
return this.pool.activeSlotByPoolIndex;
|
|
666
|
+
}
|
|
667
|
+
/** Rows in the pool backing this mesh. */
|
|
668
|
+
get poolRows() {
|
|
669
|
+
return this.pool.rows;
|
|
670
|
+
}
|
|
671
|
+
get centersTexture() {
|
|
672
|
+
return this.pool.centersTexture;
|
|
673
|
+
}
|
|
674
|
+
get backing() {
|
|
675
|
+
return this.pool.backing;
|
|
676
|
+
}
|
|
677
|
+
get freeRowSpans() {
|
|
678
|
+
return this.pool.freeRowSpans;
|
|
679
|
+
}
|
|
680
|
+
set freeRowSpans(e) {
|
|
681
|
+
this.pool.freeRowSpans = e;
|
|
682
|
+
}
|
|
683
|
+
get poolFloatTextures() {
|
|
684
|
+
return this.pool.floatTextures;
|
|
685
|
+
}
|
|
686
|
+
/** Bands of per-splat (non-palette) SH this pool stores; 0 when disabled. */
|
|
687
|
+
get packedShBands() {
|
|
688
|
+
return this.pool.packedShBands;
|
|
689
|
+
}
|
|
690
|
+
get shPackedTextures() {
|
|
691
|
+
return this.pool.shPackedTextures;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Higher-order SH bands this mesh actually renders (0 when it has none),
|
|
695
|
+
* whatever the source: a static mesh's palette shN, or per-splat SH in a
|
|
696
|
+
* dynamic pool. Reflects what was resolved at construction, not what was
|
|
697
|
+
* asked for - useful for a debug readout.
|
|
698
|
+
*/
|
|
699
|
+
get shBands() {
|
|
700
|
+
var e;
|
|
701
|
+
return ((e = this.materialInputs.sh) == null ? void 0 : e.bands) ?? 0;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* The contribution-culling profile currently in effect (resolved at
|
|
705
|
+
* construction, or the last {@link setPerformanceProfile} value).
|
|
706
|
+
*/
|
|
707
|
+
get performanceProfile() {
|
|
708
|
+
return this.performanceProfileValue;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Changes contribution culling without changing scene data or persisted
|
|
712
|
+
* settings.
|
|
713
|
+
*
|
|
714
|
+
* Mutator convention: state that is plain data is a property (get/set pair);
|
|
715
|
+
* a `setX` method exists where the write has behavior - this one rebuilds the
|
|
716
|
+
* material graph, {@link StreamedSplatMesh.setBudget} returns its clamp.
|
|
717
|
+
* Every mutator has a matching readable property.
|
|
718
|
+
*/
|
|
719
|
+
setPerformanceProfile(e) {
|
|
720
|
+
e !== this.performanceProfileValue && (this.performanceProfileValue = e, this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh));
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Gaussian cutoff radius currently in effect, in standard deviations.
|
|
724
|
+
* Resolved at construction, or the last {@link setMaxStdDev} value.
|
|
725
|
+
*/
|
|
726
|
+
get maxStdDev() {
|
|
727
|
+
return this.maxStdDevValue;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Changes the Gaussian cutoff without changing scene data.
|
|
731
|
+
*
|
|
732
|
+
* Lowering it shrinks every quad and is the main fill-rate lever in a busy
|
|
733
|
+
* view; raising it restores the faint outer tail. Rebuilds the material
|
|
734
|
+
* graph, same convention as {@link setPerformanceProfile}.
|
|
735
|
+
*/
|
|
736
|
+
setMaxStdDev(e) {
|
|
737
|
+
const t = ct(e);
|
|
738
|
+
t === void 0 || t === this.maxStdDevValue || (this.maxStdDevValue = t, this.cachedUnifiedView = null, this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh));
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Copies a decoded scene (or chunk) into the pool and activates it.
|
|
742
|
+
* Ranges are row-aligned in the pool textures, so each allocation may
|
|
743
|
+
* round up to the next multiple of the texture width internally.
|
|
744
|
+
*
|
|
745
|
+
* @returns A handle to pass to {@link removeRange}.
|
|
746
|
+
* @throws {Error} when the remaining pool capacity cannot fit the range.
|
|
747
|
+
*/
|
|
748
|
+
appendRange(e) {
|
|
749
|
+
return this.appendRangeWithState(e, !0);
|
|
750
|
+
}
|
|
751
|
+
/** Appends uploaded data without exposing it to draw/sort until activated. */
|
|
752
|
+
appendInactiveRange(e) {
|
|
753
|
+
return this.appendRangeWithState(e, !1);
|
|
754
|
+
}
|
|
755
|
+
/** Reserves an inactive pool range whose data can be filled over multiple frames. */
|
|
756
|
+
reserveInactiveRange(e) {
|
|
757
|
+
if (!Number.isInteger(e) || e < 0)
|
|
758
|
+
throw new RangeError("SplatMesh.reserveInactiveRange: count must be a non-negative integer.");
|
|
759
|
+
if (e === 0) {
|
|
760
|
+
const a = Object.freeze({ count: 0 });
|
|
761
|
+
return this.ranges.set(a, { startRow: 0, rowCount: 0, start: 0, count: 0, active: !1 }), a;
|
|
762
|
+
}
|
|
763
|
+
const t = v.DATA_TEXTURE_WIDTH, i = Math.ceil(e / t), s = Q(this.freeRowSpans, i, this.poolRows), r = s * t, n = Object.freeze({ count: e });
|
|
764
|
+
this.ranges.set(n, { startRow: s, rowCount: i, start: r, count: e, active: !1 });
|
|
765
|
+
for (const a of this.channels.values())
|
|
766
|
+
a.backing.fill(a.fill, r, r + i * t), a.pendingRows.push({ start: s, count: i });
|
|
767
|
+
return n;
|
|
768
|
+
}
|
|
769
|
+
/** Writes one contiguous segment of a reserved inactive range. */
|
|
770
|
+
writeInactiveRange(e, t, i) {
|
|
771
|
+
const s = this.ranges.get(e);
|
|
772
|
+
if (!s) throw new Error("SplatMesh.writeInactiveRange: unknown range handle.");
|
|
773
|
+
if (s.active) throw new Error("SplatMesh.writeInactiveRange: range is already active.");
|
|
774
|
+
if (!Number.isInteger(i) || i < 0 || i + t.count > s.count)
|
|
775
|
+
throw new RangeError("SplatMesh.writeInactiveRange: write exceeds the reserved range.");
|
|
776
|
+
if (t.count === 0) return;
|
|
777
|
+
this.warnOnIgnoredSh(t, "writeInactiveRange");
|
|
778
|
+
const r = s.start + i;
|
|
779
|
+
this.writeSplatRows(r, t);
|
|
780
|
+
const n = v.DATA_TEXTURE_WIDTH, a = Math.floor(r / n), c = Math.floor((r + t.count - 1) / n);
|
|
781
|
+
this.markRowsWritten(a, c - a + 1), F.setFromArray(t.positions), this.localBounds.union(F), this.boundsDirty = !0;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Overwrites splats at `offset` within a range **in place** - unlike
|
|
785
|
+
* {@link writeInactiveRange} this allows an *active* range, so the frontier
|
|
786
|
+
* page table can page individual slots of its always-active slab. Queues the
|
|
787
|
+
* touched rows for upload; the caller must {@link invalidateSort} once per
|
|
788
|
+
* batch since splat depths changed.
|
|
789
|
+
*/
|
|
790
|
+
overwriteRangeData(e, t, i) {
|
|
791
|
+
const s = this.ranges.get(e);
|
|
792
|
+
if (!s) throw new Error("SplatMesh.overwriteRangeData: unknown range handle.");
|
|
793
|
+
if (!Number.isInteger(i) || i < 0 || i + t.count > s.count)
|
|
794
|
+
throw new RangeError("SplatMesh.overwriteRangeData: write exceeds the range.");
|
|
795
|
+
if (t.count === 0) return;
|
|
796
|
+
this.warnOnIgnoredSh(t, "overwriteRangeData");
|
|
797
|
+
const r = s.start + i;
|
|
798
|
+
this.writeSplatRows(r, t);
|
|
799
|
+
const n = v.DATA_TEXTURE_WIDTH, a = Math.floor(r / n), c = Math.floor((r + t.count - 1) / n);
|
|
800
|
+
this.markRowsWritten(a, c - a + 1), F.setFromArray(t.positions), this.localBounds.union(F), this.boundsDirty = !0, this.contentRevision++;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Zeros splats `[offset, offset + count)` of a range so they draw nothing
|
|
804
|
+
* (zero covariance → degenerate quad, zero color). Used to free frontier slab
|
|
805
|
+
* slots that leave the frontier. Queues the rows for upload.
|
|
806
|
+
*/
|
|
807
|
+
degenerateRange(e, t, i) {
|
|
808
|
+
const s = this.ranges.get(e);
|
|
809
|
+
if (!s) throw new Error("SplatMesh.degenerateRange: unknown range handle.");
|
|
810
|
+
if (i <= 0) return;
|
|
811
|
+
if (!Number.isInteger(t) || t < 0 || t + i > s.count)
|
|
812
|
+
throw new RangeError("SplatMesh.degenerateRange: range out of bounds.");
|
|
813
|
+
const r = s.start + t, { centers: n, colors: a, covarianceA: c, covarianceB: h } = this.backing;
|
|
814
|
+
n.fill(0, r * 4, (r + i) * 4), a.fill(0, r * 4, (r + i) * 4), c.fill(0, r * 4, (r + i) * 4), h.fill(0, r * 4, (r + i) * 4);
|
|
815
|
+
const l = v.DATA_TEXTURE_WIDTH, f = Math.floor(r / l), x = Math.floor((r + i - 1) / l);
|
|
816
|
+
this.markRowsWritten(f, x - f + 1), this.contentRevision++;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Copies one chunk's splats into the pool's backing arrays at `destination`.
|
|
820
|
+
* Shared by the append and staged-write paths, which differ only in where
|
|
821
|
+
* the rows come from and when they are activated.
|
|
822
|
+
*/
|
|
823
|
+
writeSplatRows(e, t) {
|
|
824
|
+
const { centers: i, colors: s, covarianceA: r, covarianceB: n } = this.backing;
|
|
825
|
+
s.set(t.colors, e * 4);
|
|
826
|
+
const a = t.count, c = t.positions, h = t.covariances, l = this.shEnabled && t.sh ? t.sh.labels : null, f = t.frontierParent ?? null;
|
|
827
|
+
for (let x = 0; x < a; x++) {
|
|
828
|
+
const p = (e + x) * 4, w = x * 3, m = x * 6;
|
|
829
|
+
i[p + 0] = c[w + 0], i[p + 1] = c[w + 1], i[p + 2] = c[w + 2], r[p + 0] = h[m + 0], r[p + 1] = h[m + 1], r[p + 2] = h[m + 2], r[p + 3] = h[m + 3], n[p + 0] = h[m + 4], n[p + 1] = h[m + 5], n[p + 2] = l ? l[x] : 0, n[p + 3] = f ? f[x] : 0;
|
|
830
|
+
}
|
|
831
|
+
this.writePackedSh(e, t);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Scatters a chunk's packed SH words into the per-group pool textures.
|
|
835
|
+
*
|
|
836
|
+
* A chunk without SH still writes: pool rows are reused, so leaving the
|
|
837
|
+
* previous occupant's words behind would give a DC-only chunk somebody
|
|
838
|
+
* else's view-dependent color. Zero is not neutral (the range is signed and
|
|
839
|
+
* rarely symmetric), so the code for "0.0" in this scene's range is.
|
|
840
|
+
*/
|
|
841
|
+
writePackedSh(e, t) {
|
|
842
|
+
if (this.packedShBands === 0) return;
|
|
843
|
+
const i = this.backing.shPacked, s = J(this.packedShBands), r = t.shPacked;
|
|
844
|
+
if (r && r.bands === this.packedShBands) {
|
|
845
|
+
this.applyShRange(r);
|
|
846
|
+
const a = this.currentShRange(), c = a !== null && !Et(r.range, a);
|
|
847
|
+
for (let h = 0; h < t.count; h++)
|
|
848
|
+
for (let l = 0; l < s; l++) {
|
|
849
|
+
const f = r.packed[h * s + l];
|
|
850
|
+
i[l >> 2][(e + h) * 4 + (l & 3)] = c ? Wt(f, r.range, a) : f;
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
this.shRangeSet || (this.wrotePreLockNeutralSh = !0);
|
|
855
|
+
const n = this.neutralShWord();
|
|
856
|
+
for (let a = 0; a < t.count; a++)
|
|
857
|
+
for (let c = 0; c < s; c++)
|
|
858
|
+
i[c >> 2][(e + a) * 4 + (c & 3)] = n;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Adopts the first packed-SH chunk's dequantization range as the scene's one
|
|
862
|
+
* pool uniform. LCC/`.rad` chunks already share one range; a streamed SOG
|
|
863
|
+
* chunk measures its own extent, so later chunks are requantized into this
|
|
864
|
+
* one at write time ({@link writePackedSh}) rather than ignored.
|
|
865
|
+
*/
|
|
866
|
+
applyShRange(e) {
|
|
867
|
+
if (!this.shRangeSet && (this.shRange.min.value.set(e.range.min[0], e.range.min[1], e.range.min[2]), this.shRange.max.value.set(e.range.max[0], e.range.max[1], e.range.max[2]), this.shRangeSet = !0, this.wrotePreLockNeutralSh)) {
|
|
868
|
+
const t = this.neutralShWord();
|
|
869
|
+
for (const i of this.backing.shPacked) i.fill(t);
|
|
870
|
+
this.markRowsWritten(0, this.poolRows), this.wrotePreLockNeutralSh = !1;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/** The scene's locked packed-SH range as a plain tuple, or null if unset. */
|
|
874
|
+
currentShRange() {
|
|
875
|
+
if (!this.shRangeSet) return null;
|
|
876
|
+
const { min: e, max: t } = this.shRange;
|
|
877
|
+
return {
|
|
878
|
+
min: [e.value.x, e.value.y, e.value.z],
|
|
879
|
+
max: [t.value.x, t.value.y, t.value.z]
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
/** The packed word decoding to 0 in every channel under the scene's range. */
|
|
883
|
+
neutralShWord() {
|
|
884
|
+
const e = this.currentShRange();
|
|
885
|
+
return e ? Lt(e) : 0;
|
|
886
|
+
}
|
|
887
|
+
/** Warns once per call site when a source's SH cannot be stored. */
|
|
888
|
+
warnOnIgnoredSh(e, t) {
|
|
889
|
+
e.sh && !this.isStatic && O(
|
|
890
|
+
`SplatMesh.${t}: palette shN data on appended ranges is ignored in dynamic-capacity mode (per-file palettes cannot be merged into a shared pool).`
|
|
891
|
+
), e.shPacked && this.packedShBands === 0 && O(
|
|
892
|
+
`SplatMesh.${t}: per-splat SH was supplied but the pool has none allocated ` + (this.isStatic ? "(the static source had no `shPacked` at construction)." : "(construct the mesh with `shBands` to store it).")
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
/** Atomically includes or excludes a resident range on the next active-list rebuild. */
|
|
896
|
+
setRangeActive(e, t) {
|
|
897
|
+
const i = this.ranges.get(e);
|
|
898
|
+
if (!i) throw new Error("SplatMesh.setRangeActive: unknown range handle.");
|
|
899
|
+
i.active !== t && (i.active = t, t ? this.activateRecord(i) : this.deactivateRecord(i), this.contentRevision++);
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Activates only the first `prefix` splats of a range (see
|
|
903
|
+
* {@link RangeRecord.activePrefix}). The page-table slab keeps its used slots
|
|
904
|
+
* densely packed at the front, so this bounds per-frame sorting and vertex
|
|
905
|
+
* work to the *drawn* frontier instead of the whole pool-sized slab. Write
|
|
906
|
+
* APIs (`overwriteRangeData` / `degenerateRange`) still address the full range.
|
|
907
|
+
*/
|
|
908
|
+
setRangeActivePrefix(e, t) {
|
|
909
|
+
const i = this.ranges.get(e);
|
|
910
|
+
if (!i) throw new Error("SplatMesh.setRangeActivePrefix: unknown range handle.");
|
|
911
|
+
const s = Math.max(0, Math.min(i.count, Math.floor(t))), r = i.active ? i.activePrefix ?? i.count : 0;
|
|
912
|
+
s !== r && (i.active && (this.deactivateRecord(i), i.active = !1), i.activePrefix = s, s > 0 && (i.active = !0, this.activateRecord(i)), this.contentRevision++);
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Replaces the drawn pool indices for an immutable static hierarchy.
|
|
916
|
+
*
|
|
917
|
+
* This is intentionally narrower than the range API: subclasses may select
|
|
918
|
+
* an arbitrary hierarchy frontier, but the referenced pool rows must remain
|
|
919
|
+
* resident for the lifetime of the mesh.
|
|
920
|
+
*/
|
|
921
|
+
replaceActiveIndices(e) {
|
|
922
|
+
const t = this.sourceIndexAttribute.array;
|
|
923
|
+
if (e.length > t.length)
|
|
924
|
+
throw new RangeError("SplatMesh.replaceActiveIndices: frontier exceeds pool capacity.");
|
|
925
|
+
this.activeSlotByPoolIndex.fill(4294967295);
|
|
926
|
+
for (let s = 0; s < e.length; s++) {
|
|
927
|
+
const r = e[s];
|
|
928
|
+
if (r >= t.length)
|
|
929
|
+
throw new RangeError(
|
|
930
|
+
"SplatMesh.replaceActiveIndices: frontier contains an invalid pool index."
|
|
931
|
+
);
|
|
932
|
+
if (this.activeSlotByPoolIndex[r] !== 4294967295)
|
|
933
|
+
throw new Error(
|
|
934
|
+
"SplatMesh.replaceActiveIndices: frontier contains duplicate pool indices."
|
|
935
|
+
);
|
|
936
|
+
t[s] = r, this.activeSlotByPoolIndex[r] = s;
|
|
937
|
+
}
|
|
938
|
+
const i = this.activeCount;
|
|
939
|
+
this.activeCount = e.length, this.commitActiveListMutation(0, Math.max(i, this.activeCount)), this.queryEpoch++, this.contentRevision++;
|
|
940
|
+
}
|
|
941
|
+
/** Fast identity-frontier variant that avoids allocating a large index array. */
|
|
942
|
+
replaceActivePrefix(e) {
|
|
943
|
+
const t = this.sourceIndexAttribute.array, i = Math.max(0, Math.min(t.length, Math.floor(e)));
|
|
944
|
+
this.activeSlotByPoolIndex.fill(4294967295);
|
|
945
|
+
for (let r = 0; r < i; r++)
|
|
946
|
+
t[r] = r, this.activeSlotByPoolIndex[r] = r;
|
|
947
|
+
const s = this.activeCount;
|
|
948
|
+
this.activeCount = i, this.commitActiveListMutation(0, Math.max(s, i)), this.queryEpoch++, this.contentRevision++;
|
|
949
|
+
}
|
|
950
|
+
/** Replaces a static mesh's resident cut without increasing its fixed pool allocation. */
|
|
951
|
+
replaceStaticData(e) {
|
|
952
|
+
if (!this.isStatic || !this.staticRange)
|
|
953
|
+
throw new Error("SplatMesh.replaceStaticData: mesh was not constructed from static data.");
|
|
954
|
+
if (e.count > this.capacity)
|
|
955
|
+
throw new RangeError("SplatMesh.replaceStaticData: cut exceeds pool capacity.");
|
|
956
|
+
this.overwriteRangeData(this.staticRange, e, 0), this.setRangeActivePrefix(this.staticRange, e.count), this.invalidateSort();
|
|
957
|
+
}
|
|
958
|
+
appendRangeWithState(e, t) {
|
|
959
|
+
if (e.count === 0) {
|
|
960
|
+
const h = Object.freeze({ count: 0 });
|
|
961
|
+
return this.ranges.set(h, { startRow: 0, rowCount: 0, start: 0, count: 0, active: t }), h;
|
|
962
|
+
}
|
|
963
|
+
const i = v.DATA_TEXTURE_WIDTH, s = Math.ceil(e.count / i), r = Q(this.freeRowSpans, s, this.poolRows), n = r * i;
|
|
964
|
+
this.warnOnIgnoredSh(e, "appendRange"), this.writeSplatRows(n, e);
|
|
965
|
+
const a = Object.freeze({ count: e.count }), c = { startRow: r, rowCount: s, start: n, count: e.count, active: t };
|
|
966
|
+
this.ranges.set(a, c), this.markRowsWritten(r, s);
|
|
967
|
+
for (const h of this.channels.values())
|
|
968
|
+
h.backing.fill(h.fill, n, n + s * i), h.pendingRows.push({ start: r, count: s });
|
|
969
|
+
return F.setFromArray(e.positions), this.localBounds.union(F), this.boundsDirty = !0, t && this.activateRecord(c), this.contentRevision++, a;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Deactivates a previously appended range and frees its pool rows for
|
|
973
|
+
* reuse. The bounding sphere used for depth quantization stays a
|
|
974
|
+
* conservative superset until new ranges grow it again.
|
|
975
|
+
*/
|
|
976
|
+
removeRange(e) {
|
|
977
|
+
const t = this.ranges.get(e);
|
|
978
|
+
if (!t) throw new Error("SplatMesh.removeRange: unknown range handle.");
|
|
979
|
+
t.active && this.deactivateRecord(t), this.ranges.delete(e), t.rowCount > 0 && (this.freeRowSpans = Mt(this.freeRowSpans, t.startRow, t.rowCount)), this.contentRevision++;
|
|
980
|
+
}
|
|
981
|
+
/** Maximum number of splats the pool can hold (row-aligned internally). */
|
|
982
|
+
get capacity() {
|
|
983
|
+
return this.poolRows * v.DATA_TEXTURE_WIDTH;
|
|
984
|
+
}
|
|
985
|
+
/** Number of splats currently active (drawn and depth-sorted). */
|
|
986
|
+
get activeSplatCount() {
|
|
987
|
+
return this.activeCount;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Returns this mesh's current pool state for an internal unified gather.
|
|
991
|
+
* Call {@link update} first for streamed sources so the active list reflects
|
|
992
|
+
* the current LOD cut. Consumers must treat the returned GPU resources as
|
|
993
|
+
* read-only; range lifecycle remains owned by this mesh.
|
|
994
|
+
*/
|
|
995
|
+
getUnifiedSourceView() {
|
|
996
|
+
this.updateWorldMatrix(!0, !1), this.refreshSortBounds();
|
|
997
|
+
const e = this.cachedUnifiedView;
|
|
998
|
+
return e !== null && e.contentRevision === this.contentRevision && e.graphRevision === this.graphRevision && e.activeCount === this.activeCount && e.sh === this.materialInputs.sh && e.modifiers === this.modifierList && e.hasSourcePlacement === (this.perSourceSort !== null) && e.centersTexture === this.centersTexture && e.colorsTexture === this.materialInputs.textures.colorsTexture && this.cachedUnifiedViewMatrixWorld.equals(this.matrixWorld) && this.cachedUnifiedViewLocalBounds.equals(this.boundingSphereLocal) ? e : (this.cachedUnifiedViewMatrixWorld.copy(this.matrixWorld), this.cachedUnifiedViewLocalBounds.copy(this.boundingSphereLocal), this.cachedUnifiedViewWorldBounds.copy(this.boundingSphereLocal).applyMatrix4(this.matrixWorld), this.cachedUnifiedView = {
|
|
999
|
+
capacity: this.capacity,
|
|
1000
|
+
sourceIndex: this.sourceIndexAttribute,
|
|
1001
|
+
activeCount: this.activeCount,
|
|
1002
|
+
centersTexture: this.centersTexture,
|
|
1003
|
+
colorsTexture: this.materialInputs.textures.colorsTexture,
|
|
1004
|
+
covarianceATexture: this.materialInputs.textures.covarianceATexture,
|
|
1005
|
+
covarianceBTexture: this.materialInputs.textures.covarianceBTexture,
|
|
1006
|
+
dataTextureWidth: v.DATA_TEXTURE_WIDTH,
|
|
1007
|
+
matrixWorld: this.matrixWorld,
|
|
1008
|
+
worldBounds: this.cachedUnifiedViewWorldBounds,
|
|
1009
|
+
sh: this.materialInputs.sh,
|
|
1010
|
+
modifiers: this.modifierList,
|
|
1011
|
+
hasSourcePlacement: this.perSourceSort !== null,
|
|
1012
|
+
channels: this.channels,
|
|
1013
|
+
localCameraPosition: this.localCameraPosition,
|
|
1014
|
+
graphRevision: this.graphRevision,
|
|
1015
|
+
srgbOutput: this.srgbOutput,
|
|
1016
|
+
maxStdDev: this.maxStdDev,
|
|
1017
|
+
minSplatSizePx: this.minSplatSizePx,
|
|
1018
|
+
antialias: this.antialias,
|
|
1019
|
+
projectedFilterProfile: this.projectedFilterProfile,
|
|
1020
|
+
// Fixed at construction, so it needs no cache-invalidation key.
|
|
1021
|
+
lodAlpha: this.lodAlpha,
|
|
1022
|
+
contentRevision: this.contentRevision
|
|
1023
|
+
}, this.cachedUnifiedView);
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Marks this mesh as a source drawn by {@link UnifiedSplatMesh}.
|
|
1027
|
+
* The source stays invisible to the regular scene draw, while its own picker
|
|
1028
|
+
* mirrors this resolved visibility so existing per-source hit testing works.
|
|
1029
|
+
* Internal consumers should clear the state with `null` before disposing.
|
|
1030
|
+
*/
|
|
1031
|
+
setUnifiedPickVisibility(e) {
|
|
1032
|
+
this.unifiedPickVisibility = e;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* The visibility that actually decides whether this mesh's splats reach the
|
|
1036
|
+
* screen: the owning {@link UnifiedSplatMesh}'s per-source visibility
|
|
1037
|
+
* while one owns the draw, else `Object3D.visible`. Consumers that gate on
|
|
1038
|
+
* "is this mesh showing" - the picker, `CameraBudgetGovernor` - must read
|
|
1039
|
+
* this rather than `visible`, which a unified renderer forces to `false` on
|
|
1040
|
+
* every source it owns purely to keep the regular scene draw from
|
|
1041
|
+
* double-drawing them.
|
|
1042
|
+
*/
|
|
1043
|
+
get effectiveVisibility() {
|
|
1044
|
+
return this.unifiedPickVisibility ?? this.visible;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Pool splats still allocatable, in whole rows. Because allocation is
|
|
1048
|
+
* row-aligned, an append of n splats fits when
|
|
1049
|
+
* `ceil(n / rowWidth) · rowWidth ≤ freeSplatCapacity` *and* a contiguous
|
|
1050
|
+
* span exists; callers use this for conservative pre-checks before
|
|
1051
|
+
* falling back to {@link compact}.
|
|
1052
|
+
*/
|
|
1053
|
+
get freeSplatCapacity() {
|
|
1054
|
+
let e = 0;
|
|
1055
|
+
for (const t of this.freeRowSpans) e += t.count;
|
|
1056
|
+
return e * v.DATA_TEXTURE_WIDTH;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Declares a named per-splat data channel - one extra value per pool slot,
|
|
1060
|
+
* in its own pool-aligned data texture. Channels are opt-in: nothing is
|
|
1061
|
+
* allocated until you call this. Read a channel from a modifier with
|
|
1062
|
+
* `ctx.channel(name)` and write values per range with {@link writeChannel}.
|
|
1063
|
+
*
|
|
1064
|
+
* Because a splat keeps its pool row for its whole residency, a channel
|
|
1065
|
+
* value follows the splat across depth re-sorts and pool {@link compact}ion
|
|
1066
|
+
* - the persistent per-splat label the SDF/selection effects build on. Call
|
|
1067
|
+
* this **before** assigning a modifier that reads the channel; a modifier
|
|
1068
|
+
* that reads an undeclared channel is a material-build error.
|
|
1069
|
+
*
|
|
1070
|
+
* @throws {Error} if a channel of this name already exists.
|
|
1071
|
+
*/
|
|
1072
|
+
defineChannel(e, t = {}) {
|
|
1073
|
+
if (this.channels.has(e))
|
|
1074
|
+
throw new Error(`SplatMesh.defineChannel: channel "${e}" already defined.`);
|
|
1075
|
+
const i = t.type ?? "float", s = v.DATA_TEXTURE_WIDTH, r = s * this.poolRows, n = i === "byte" ? new Uint8Array(r) : new Float32Array(r), a = t.fill ?? 0;
|
|
1076
|
+
a && n.fill(a);
|
|
1077
|
+
const c = i === "byte" ? d.UnsignedByteType : d.FloatType, h = new d.DataTexture(
|
|
1078
|
+
n,
|
|
1079
|
+
s,
|
|
1080
|
+
this.poolRows,
|
|
1081
|
+
d.RedFormat,
|
|
1082
|
+
c
|
|
1083
|
+
);
|
|
1084
|
+
h.needsUpdate = !0, this.channels.set(e, { type: i, fill: a, backing: n, texture: h, textureType: c, pendingRows: [] });
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Writes channel values for a resident range, at `[offset, offset + data.length)`
|
|
1088
|
+
* splats within it (default `offset` 0). Values ride the same staging-upload
|
|
1089
|
+
* path as pool data, flushed on the next {@link update}. `byte` channels take
|
|
1090
|
+
* raw `0..255`; `float` channels take the value verbatim.
|
|
1091
|
+
*
|
|
1092
|
+
* @throws {Error} for an unknown channel or range, or a write past the range.
|
|
1093
|
+
*/
|
|
1094
|
+
writeChannel(e, t, i, s = 0) {
|
|
1095
|
+
const r = this.channels.get(t);
|
|
1096
|
+
if (!r) throw new Error(`SplatMesh.writeChannel: channel "${t}" is not defined.`);
|
|
1097
|
+
const n = this.ranges.get(e);
|
|
1098
|
+
if (!n) throw new Error("SplatMesh.writeChannel: unknown range handle.");
|
|
1099
|
+
if (s < 0 || s + i.length > n.count)
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`SplatMesh.writeChannel: write of ${i.length} at offset ${s} exceeds range count ${n.count}.`
|
|
1102
|
+
);
|
|
1103
|
+
if (i.length === 0) return;
|
|
1104
|
+
const a = v.DATA_TEXTURE_WIDTH, c = n.start + s;
|
|
1105
|
+
r.backing.set(i, c);
|
|
1106
|
+
const h = Math.floor(c / a), l = Math.floor((c + i.length - 1) / a);
|
|
1107
|
+
r.pendingRows.push({ start: h, count: l - h + 1 }), this.contentRevision++;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Effect hooks, folded into the vertex stage in array order - each
|
|
1111
|
+
* modifier sees the running result of the ones before it. Assigning a
|
|
1112
|
+
* changed list rebuilds the material (a pipeline recompile); animating a
|
|
1113
|
+
* modifier's own uniforms or storage buffers never does. With an empty
|
|
1114
|
+
* list the material is exactly the unhooked renderer.
|
|
1115
|
+
* See `docs/guide/effects-and-modifiers.md`.
|
|
1116
|
+
*/
|
|
1117
|
+
get modifiers() {
|
|
1118
|
+
return this.modifierList;
|
|
1119
|
+
}
|
|
1120
|
+
set modifiers(e) {
|
|
1121
|
+
if (e.length === this.modifierList.length && e.every((s, r) => s === this.modifierList[r])) return;
|
|
1122
|
+
const i = this.modifierList;
|
|
1123
|
+
this.modifierList = [...e];
|
|
1124
|
+
try {
|
|
1125
|
+
this.rebuildGraph();
|
|
1126
|
+
} catch (s) {
|
|
1127
|
+
throw this.modifierList = i, this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh), s;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Recompiles the material graph from the mesh's current inputs and publishes
|
|
1132
|
+
* the rebuild (`needsUpdate`, `graphRevision`, picker invalidation).
|
|
1133
|
+
*
|
|
1134
|
+
* A seam rather than inline code because a subclass can change a *build
|
|
1135
|
+
* input* without changing the modifier list - `MergedSplatMesh` installs its
|
|
1136
|
+
* per-source placement in the constructor, after the base class has already
|
|
1137
|
+
* built a placement-free graph. Note `defineChannel` deliberately does *not*
|
|
1138
|
+
* rebuild; only structural graph changes come through here.
|
|
1139
|
+
*
|
|
1140
|
+
* @internal
|
|
1141
|
+
*/
|
|
1142
|
+
rebuildGraph() {
|
|
1143
|
+
this.buildMaterial(this.materialInputs.textures, this.materialInputs.sh), this.material.needsUpdate = !0, this.graphRevision++, this.picker.markNeedsUpdate();
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Core projected-2D depth of field. Adds an isotropic screen-space CoC disc
|
|
1147
|
+
* after EWA projection (not the stylized M13 scale modifier). Live uniforms
|
|
1148
|
+
* - no material rebuild. Pass `aperture: 0` to disable.
|
|
1149
|
+
*/
|
|
1150
|
+
setDepthOfField(e) {
|
|
1151
|
+
const t = It(e, this.getDepthOfField());
|
|
1152
|
+
this.dofFocusDistance.value = t.focusDistance, this.dofAperture.value = t.aperture;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* PlayCanvas-style proxy-mesh relighting. Multiplies baked splat color in the
|
|
1156
|
+
* **display** fragment by a screen-space sample of `map` (RGB = lit proxy,
|
|
1157
|
+
* A = coverage). Pass `null` to disable (`blend → 0`, placeholder map).
|
|
1158
|
+
*
|
|
1159
|
+
* Blend / brightness / background are live uniforms (no rebuild). Changing
|
|
1160
|
+
* the map texture identity rebuilds the material once. Does not affect the
|
|
1161
|
+
* pick pass. Does not invalidate a unified gather cache.
|
|
1162
|
+
*/
|
|
1163
|
+
setRelighting(e) {
|
|
1164
|
+
if (e === null) {
|
|
1165
|
+
this.relightBlend.value = 0, this.relightMap !== this.relightPlaceholder && (this.relightMap = this.relightPlaceholder, this.rebuildGraph());
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const t = kt(e, this.getRelighting());
|
|
1169
|
+
this.relightBlend.value = t.blend, this.relightBrightness.value = t.brightness, this.relightBackground.value = t.background, this.relightSoftness.value = t.softness, e.map !== this.relightMap && (this.relightMap = e.map, this.rebuildGraph());
|
|
1170
|
+
}
|
|
1171
|
+
/** Current relight numeric uniforms (`blend === 0` means off). */
|
|
1172
|
+
getRelighting() {
|
|
1173
|
+
return {
|
|
1174
|
+
blend: this.relightBlend.value,
|
|
1175
|
+
brightness: this.relightBrightness.value,
|
|
1176
|
+
background: this.relightBackground.value,
|
|
1177
|
+
softness: this.relightSoftness.value
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Moves the screen-radius band (px) without rebuilding the material.
|
|
1182
|
+
*
|
|
1183
|
+
* Only meaningful on a mesh constructed *with* a band - the graph decides at
|
|
1184
|
+
* build time whether to cull on radius at all, from
|
|
1185
|
+
* {@link SplatMeshOptions.minSplatScreenRadius} / `maxSplatScreenRadius`.
|
|
1186
|
+
*
|
|
1187
|
+
* A foveated mesh uses this to keep the band aligned with its LOD cut: the
|
|
1188
|
+
* band spans roughly one level, so refining the cut to spend spare budget
|
|
1189
|
+
* selects smaller splats, and a band left where it was would cull precisely
|
|
1190
|
+
* the detail that refinement bought.
|
|
1191
|
+
*/
|
|
1192
|
+
setScreenRadiusBand(e, t) {
|
|
1193
|
+
this.screenBandMin.value = Math.max(0, e), this.screenBandMax.value = Math.max(0, t);
|
|
1194
|
+
}
|
|
1195
|
+
/** Current core DoF uniforms (`aperture === 0` means off). */
|
|
1196
|
+
getDepthOfField() {
|
|
1197
|
+
return {
|
|
1198
|
+
focusDistance: this.dofFocusDistance.value,
|
|
1199
|
+
aperture: this.dofAperture.value
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Packs the resident ranges toward the start of the pool, removing the
|
|
1204
|
+
* gaps that add/remove churn leaves behind and restoring a single
|
|
1205
|
+
* contiguous free span. Use this when {@link appendRange} throws despite
|
|
1206
|
+
* enough total free rows (row-alignment fragmentation).
|
|
1207
|
+
*
|
|
1208
|
+
* The CPU backing arrays are authoritative, so no data is re-fetched;
|
|
1209
|
+
* moved rows are re-uploaded to the GPU on the next {@link update}.
|
|
1210
|
+
*/
|
|
1211
|
+
compact() {
|
|
1212
|
+
this.pool.compact();
|
|
1213
|
+
}
|
|
1214
|
+
/** {@link SplatPoolTenant}: the ranges this mesh holds in the pool. */
|
|
1215
|
+
poolRanges() {
|
|
1216
|
+
return this.ranges.values();
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* {@link SplatPoolTenant}: follow one range to its new rows. The pool has
|
|
1220
|
+
* already moved the splat data; this moves what the mesh keys by pool row.
|
|
1221
|
+
*/
|
|
1222
|
+
relocatePoolRange(e, t) {
|
|
1223
|
+
const i = e, s = v.DATA_TEXTURE_WIDTH;
|
|
1224
|
+
for (const r of this.channels.values()) {
|
|
1225
|
+
const n = i.startRow * s, a = t * s, c = i.rowCount * s;
|
|
1226
|
+
r.backing.copyWithin(a, n, n + c), r.pendingRows.push({ start: t, count: i.rowCount });
|
|
1227
|
+
}
|
|
1228
|
+
i.startRow = t, i.start = t * s, this.markRowsWritten(t, i.rowCount);
|
|
1229
|
+
}
|
|
1230
|
+
/** {@link SplatPoolTenant}: rebuild everything keyed by pool index. */
|
|
1231
|
+
onPoolCompacted() {
|
|
1232
|
+
this.queryEpoch++, this.rebuildActiveList(), this.contentRevision++;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Per-frame update: uploads pending pool writes, refreshes the projection
|
|
1236
|
+
* uniforms and requests a depth re-sort when the camera or the content
|
|
1237
|
+
* has changed. Call this every frame, before `renderer.render`.
|
|
1238
|
+
*/
|
|
1239
|
+
update(e, t, i = {}) {
|
|
1240
|
+
if (this.disposed) return;
|
|
1241
|
+
this.lastRenderer = t, this.assertPoolFitsDevice(t), e.updateMatrixWorld(), this.updateWorldMatrix(!0, !1), this.updateTimings.activeListMs = 0, this.updateTimings.uploadMs = 0, this.updateTimings.sortSubmitMs = 0, this.updateTimings.stagingTextureAllocations = 0, this.updateTimings.activeListUpdateRanges = 0;
|
|
1242
|
+
const s = performance.now();
|
|
1243
|
+
this.flushPendingUploads(t), this.updateTimings.uploadMs = performance.now() - s;
|
|
1244
|
+
const r = Pt(e, t);
|
|
1245
|
+
let n = e, a = e, c, h;
|
|
1246
|
+
if (r ? (n = r.eye, a = r.head, c = r.width, h = r.height) : (t.getDrawingBufferSize(P), c = P.x, h = P.y), this.adaptFoveationLimit(n, h), this.writeViewUniforms(n, c, h, a), this.updateTimings.activeListUpdateRanges = this.sourceIndexAttribute.updateRanges.length, i.sort !== !1) {
|
|
1247
|
+
const l = performance.now();
|
|
1248
|
+
this.requestSortIfNeeded(a, t), this.updateTimings.sortSubmitMs = performance.now() - l;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/** Returns the render-preparation CPU timings for the current update. */
|
|
1252
|
+
getUpdateTimings() {
|
|
1253
|
+
return this.updateTimings;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Skips one otherwise ordinary sort request, but only while the active list
|
|
1257
|
+
* is unchanged since the last sort. Streamed staging uses this immediately
|
|
1258
|
+
* before a forced commit sort so the GPU queue has a frame to drain; a
|
|
1259
|
+
* concurrent swap group that mutates the active list revokes the request,
|
|
1260
|
+
* because the depth order must always describe the list being drawn.
|
|
1261
|
+
*/
|
|
1262
|
+
deferNextSortRequest() {
|
|
1263
|
+
this.deferSortRequestOnce = !0;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Once, on the first update (when the backend is known), fails loudly if the
|
|
1267
|
+
* pool's `2048 × poolRows` data textures exceed the device's maximum texture
|
|
1268
|
+
* dimension - otherwise the upload fails with a cryptic backend error and a
|
|
1269
|
+
* frozen canvas. Streamed scenes stay under the limit (budget-bounded); this
|
|
1270
|
+
* guards very large *static* scenes, especially on mobile (≈8192 max → about
|
|
1271
|
+
* 16.7 M splats). If the limit can't be read, the check is skipped rather
|
|
1272
|
+
* than risk a false rejection.
|
|
1273
|
+
*/
|
|
1274
|
+
assertPoolFitsDevice(e) {
|
|
1275
|
+
if (this.textureLimitChecked) return;
|
|
1276
|
+
this.textureLimitChecked = !0;
|
|
1277
|
+
const t = Ct(e);
|
|
1278
|
+
if (t > 0 && this.poolRows > t) {
|
|
1279
|
+
const i = v.DATA_TEXTURE_WIDTH;
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
`SplatMesh: this scene needs a ${i}×${this.poolRows} data texture, but this device caps texture dimensions at ${t} (about ${(t * i).toLocaleString("en-US")} splats). Stream it with StreamedSplatMesh, or reduce the splat count.`
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
/** Bounding box of the appended splats, in this mesh's local space. */
|
|
1286
|
+
computeSplatBounds() {
|
|
1287
|
+
return this.localBounds.clone();
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Asynchronously picks the frontmost visible splat under an NDC coordinate.
|
|
1291
|
+
*
|
|
1292
|
+
* Renders a one-pixel GPU depth pass (shared projection/Gaussian math with
|
|
1293
|
+
* the display material), reads the encoded view depth, and reconstructs a
|
|
1294
|
+
* world-space point on the splat's rendered center plane. Concurrent calls
|
|
1295
|
+
* on this mesh are serialized. Returns `null` for misses, empty meshes,
|
|
1296
|
+
* out-of-canvas coordinates, or after {@link dispose}.
|
|
1297
|
+
*
|
|
1298
|
+
* Does not return a persistent splat id - LOD streaming and pool reuse make
|
|
1299
|
+
* identity unstable across frames.
|
|
1300
|
+
*/
|
|
1301
|
+
pick(e, t, i, s) {
|
|
1302
|
+
return this.picker.pick(e, t, i, s);
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* The resident splat center nearest a world point, within `radius` (world
|
|
1306
|
+
* units), or `null` if none. A synchronous CPU query over the pool's decoded
|
|
1307
|
+
* centers - no GPU round-trip - backed by a uniform grid rebuilt only when
|
|
1308
|
+
* the resident set changes (M9). The primitive behind measurement markers and
|
|
1309
|
+
* proximity tests.
|
|
1310
|
+
*
|
|
1311
|
+
* **Resident-only:** a {@link StreamedSplatMesh} searches only the splats
|
|
1312
|
+
* currently in the pool (the LOD the camera has resolved), so the answer is
|
|
1313
|
+
* the nearest *loaded* splat - coarser far from the camera, absent where no
|
|
1314
|
+
* chunk has streamed in. A static mesh searches its whole scene.
|
|
1315
|
+
*
|
|
1316
|
+
* World-correct under any rotation + per-axis (including non-uniform) scale:
|
|
1317
|
+
* candidates are gathered in local space with a conservative radius
|
|
1318
|
+
* (`radius / min axis scale`) and then ranked by **world** distance, so the
|
|
1319
|
+
* nearest-in-world splat wins even when the axes stretch differently. Shear
|
|
1320
|
+
* in an ancestor transform is not supported. Splats displaced by GPU
|
|
1321
|
+
* modifiers are queried at their undisplaced CPU positions.
|
|
1322
|
+
*/
|
|
1323
|
+
queryNearest(e, t) {
|
|
1324
|
+
if (this.activeCount === 0 || !(t >= 0)) return null;
|
|
1325
|
+
this.updateWorldMatrix(!0, !1), T.copy(e), this.worldToLocal(T);
|
|
1326
|
+
const i = this.ensureQueryGrid(), s = t / this.queryWorldMinScale(), r = t * t;
|
|
1327
|
+
let n = -1, a = 1 / 0;
|
|
1328
|
+
return i.forEachWithin(T.x, T.y, T.z, s, (h) => {
|
|
1329
|
+
this.splatWorldPosition(h, k);
|
|
1330
|
+
const l = k.distanceToSquared(e);
|
|
1331
|
+
l <= r && l < a && (a = l, n = h);
|
|
1332
|
+
}), n < 0 ? null : { point: this.splatWorldPosition(n, new d.Vector3()), distance: Math.sqrt(a) };
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Returns the first resident splat center inside a world-space ray cone.
|
|
1336
|
+
* This is a synchronous CPU query, intended for interactions that cannot
|
|
1337
|
+
* wait for (or rely on) a GPU readback. `radiusAtUnitDistance` is the cone's
|
|
1338
|
+
* world-space radius one unit from the origin; `minimumRadius` keeps nearby
|
|
1339
|
+
* point-like splats practical to target.
|
|
1340
|
+
*
|
|
1341
|
+
* Like {@link queryNearest}, this searches resident, undisplaced CPU centers.
|
|
1342
|
+
*/
|
|
1343
|
+
queryRay(e, t = 0.025, i = 0.05) {
|
|
1344
|
+
if (this.activeCount === 0 || !(t >= 0) || !(i >= 0) || e.direction.lengthSq() === 0)
|
|
1345
|
+
return null;
|
|
1346
|
+
this.updateWorldMatrix(!0, !1);
|
|
1347
|
+
const s = this.sourceIndexAttribute.array.subarray(0, this.activeCount);
|
|
1348
|
+
let r = -1, n = 1 / 0;
|
|
1349
|
+
for (const a of s) {
|
|
1350
|
+
this.splatWorldPosition(a, k), T.subVectors(k, e.origin);
|
|
1351
|
+
const c = T.dot(e.direction);
|
|
1352
|
+
if (c < 0 || c >= n) continue;
|
|
1353
|
+
T.addScaledVector(e.direction, -c);
|
|
1354
|
+
const h = Math.max(i, c * t);
|
|
1355
|
+
T.lengthSq() <= h * h && (r = a, n = c);
|
|
1356
|
+
}
|
|
1357
|
+
return r < 0 ? null : {
|
|
1358
|
+
point: this.splatWorldPosition(r, new d.Vector3()),
|
|
1359
|
+
distance: n
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* The supporting surface beneath a world point: the highest resident splat
|
|
1364
|
+
* that lies no more than `maxDrop` below it (world −Y) and within `radius`
|
|
1365
|
+
* horizontally, or `null` if none. A downward probe over splat centers - the
|
|
1366
|
+
* primitive behind floor-following and teleport validation - without a GPU
|
|
1367
|
+
* pick or a collision mesh (M9), so it works for every format, not only the
|
|
1368
|
+
* `.lcc2` captures that ship collision geometry.
|
|
1369
|
+
*
|
|
1370
|
+
* `radius` (default `maxDrop / 2`) is the horizontal tolerance: splats are
|
|
1371
|
+
* points, so an exact vertical hit is unlikely and a small disc is searched.
|
|
1372
|
+
* Resident-only semantics and the world-correctness contract (rotation +
|
|
1373
|
+
* per-axis scale supported, shear not) are the same as {@link queryNearest};
|
|
1374
|
+
* every candidate is judged in world space, where the −Y probe direction is
|
|
1375
|
+
* defined, so the vertical test never assumes local axis alignment.
|
|
1376
|
+
*/
|
|
1377
|
+
queryHeight(e, t, i = t / 2) {
|
|
1378
|
+
if (this.activeCount === 0 || !(t >= 0) || !(i >= 0)) return null;
|
|
1379
|
+
this.updateWorldMatrix(!0, !1), T.copy(e), this.worldToLocal(T);
|
|
1380
|
+
const s = this.ensureQueryGrid(), r = (t + i) / this.queryWorldMinScale(), n = i * i;
|
|
1381
|
+
let a = -1 / 0, c = -1;
|
|
1382
|
+
if (s.forEachWithin(T.x, T.y, T.z, r, (l) => {
|
|
1383
|
+
this.splatWorldPosition(l, k);
|
|
1384
|
+
const f = e.y - k.y;
|
|
1385
|
+
if (f < 0 || f > t) return;
|
|
1386
|
+
const x = k.x - e.x, p = k.z - e.z;
|
|
1387
|
+
x * x + p * p > n || k.y > a && (a = k.y, c = l);
|
|
1388
|
+
}), c < 0) return null;
|
|
1389
|
+
const h = this.splatWorldPosition(c, new d.Vector3());
|
|
1390
|
+
return { point: h, drop: e.y - h.y };
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Renders this mesh from a second camera - into `target`, or the canvas when
|
|
1394
|
+
* omitted - with depth order **and** projection correct for *that* camera
|
|
1395
|
+
* (M10). The primitive behind mirrors, portals, split panes, and thumbnails.
|
|
1396
|
+
*
|
|
1397
|
+
* Depth order is view-dependent, but a mesh keeps one sorted order buffer:
|
|
1398
|
+
* `update()` sorts it for the single camera it is given, so a second view
|
|
1399
|
+
* drawn with that order shows transparency/pop errors. `renderView` re-sorts
|
|
1400
|
+
* for `camera` into the shared buffer, sets the view-dependent uniforms
|
|
1401
|
+
* (viewport from `target`), draws, and marks the primary order stale so the
|
|
1402
|
+
* next `update()` re-sorts for the main camera. GPU submissions execute in
|
|
1403
|
+
* order, so each view's draw reads the order and uniforms it just wrote.
|
|
1404
|
+
*
|
|
1405
|
+
* Call once per extra view per frame, before or after the primary
|
|
1406
|
+
* `update()` + render - both are correct. On the WebGL2 fallback the sorter
|
|
1407
|
+
* is an asynchronous worker owned by the primary view, so `renderView` does
|
|
1408
|
+
* not sort at all there: the secondary view draws with the primary view's
|
|
1409
|
+
* order (projection is still per-camera correct) - a documented single-view
|
|
1410
|
+
* ordering limitation; WebGPU gets exact per-view order.
|
|
1411
|
+
*
|
|
1412
|
+
* @param target - Destination render target, or `null`/omitted for the canvas.
|
|
1413
|
+
*/
|
|
1414
|
+
renderView(e, t, i = null) {
|
|
1415
|
+
if (this.disposed) return;
|
|
1416
|
+
this.lastRenderer = t, e.updateMatrixWorld(), this.updateWorldMatrix(!0, !1), this.flushPendingUploads(t), i ? P.set(i.width, i.height) : t.getDrawingBufferSize(P), this.writeViewUniforms(e, P.x, P.y);
|
|
1417
|
+
const s = this.sortForView(e, t), r = t.getRenderTarget();
|
|
1418
|
+
try {
|
|
1419
|
+
t.setRenderTarget(i), t.render(this, e);
|
|
1420
|
+
} finally {
|
|
1421
|
+
t.setRenderTarget(r), s && (this.orderIsForeign = !0);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Sorts the shared order buffer for a secondary view's camera, bypassing the
|
|
1426
|
+
* primary view's sort scheduler and its `lastSortedModelView` record (that
|
|
1427
|
+
* state belongs to `update()`'s camera). WebGPU dispatches synchronously into
|
|
1428
|
+
* the render queue, so the following draw reads this order.
|
|
1429
|
+
*/
|
|
1430
|
+
sortForView(e, t) {
|
|
1431
|
+
return this.activeCount === 0 || !(t.backend.isWebGPUBackend === !0) || (this.currentModelView.multiplyMatrices(e.matrixWorldInverse, this.matrixWorld), this.refreshSortBounds(), this.sorter ?? (this.sorter = this.createSorter(t)), !this.sorter) ? !1 : (this.sorter.sort(this.currentModelView, this.activeCount, this.boundingSphereLocal), !0);
|
|
1432
|
+
}
|
|
1433
|
+
/** Rebuilds the query grid if the resident set changed since it was built. */
|
|
1434
|
+
ensureQueryGrid() {
|
|
1435
|
+
if (this.queryGrid && this.queryGridEpoch === this.queryEpoch) return this.queryGrid;
|
|
1436
|
+
const e = this.sourceIndexAttribute.array.subarray(0, this.activeCount);
|
|
1437
|
+
return this.queryGrid = new $t(this.backing.centers, e, this.activeCount), this.queryGridEpoch = this.queryEpoch, this.queryGrid;
|
|
1438
|
+
}
|
|
1439
|
+
/** World-space position of pool splat `poolIndex`, written into `out`. */
|
|
1440
|
+
splatWorldPosition(e, t) {
|
|
1441
|
+
const i = e * 4;
|
|
1442
|
+
return t.set(
|
|
1443
|
+
this.backing.centers[i],
|
|
1444
|
+
this.backing.centers[i + 1],
|
|
1445
|
+
this.backing.centers[i + 2]
|
|
1446
|
+
), t.applyMatrix4(this.matrixWorld);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* The smallest world axis scale, for conservatively mapping a world radius
|
|
1450
|
+
* into local space: a world sphere of radius `r` fits inside a local sphere
|
|
1451
|
+
* of `r / minAxisScale`, for any per-axis (non-uniform) scale.
|
|
1452
|
+
*/
|
|
1453
|
+
queryWorldMinScale() {
|
|
1454
|
+
return this.getWorldScale(q), Math.min(Math.abs(q.x), Math.abs(q.y), Math.abs(q.z)) || 1;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Releases every GPU resource and worker this mesh owns. Idempotent: a
|
|
1458
|
+
* second call is a no-op, and a render loop that outlives the mesh by a
|
|
1459
|
+
* frame is safe - {@link update}, {@link renderView} and {@link pick} all
|
|
1460
|
+
* become no-ops after dispose.
|
|
1461
|
+
*/
|
|
1462
|
+
dispose() {
|
|
1463
|
+
var e;
|
|
1464
|
+
if (!this.disposed) {
|
|
1465
|
+
this.disposed = !0, (e = this.sorter) == null || e.dispose(), this.sorter = null, this.geometry.dispose(), this.material.dispose(), this.picker.dispose(), this.relightPlaceholder.dispose();
|
|
1466
|
+
for (const t of this.ranges.values())
|
|
1467
|
+
this.pool.releaseRows(t.startRow, t.rowCount);
|
|
1468
|
+
this.pool.unregister(this), this.ownsPool && this.pool.dispose();
|
|
1469
|
+
for (const t of this.dataTextures)
|
|
1470
|
+
!this.ownsPool && this.pool.isPoolTexture(t) || t.dispose();
|
|
1471
|
+
for (const t of this.channels.values()) t.texture.dispose();
|
|
1472
|
+
this.channels.clear();
|
|
1473
|
+
for (const t of this.uploadStaging.values())
|
|
1474
|
+
for (const i of t.values()) i.dispose();
|
|
1475
|
+
this.uploadStaging.clear(), this.halfEncodeBuffers.clear(), this.lastRenderer && (Ut(this.lastRenderer, [this.sourceIndexAttribute]), this.lastRenderer = null), this.pendingUploadRows = [], this.workerDirtyRows = [], this.queryGrid = null, this.ranges.clear();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
/** Matches the viewport / focal / local-camera uniforms written by {@link update}. */
|
|
1479
|
+
refreshProjectionUniforms(e, t) {
|
|
1480
|
+
t.getDrawingBufferSize(P), this.writeViewUniforms(e, P.x, P.y);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Writes the three view-dependent uniforms - viewport, focal (splat screen
|
|
1484
|
+
* size), and the camera position in mesh-local space (SH view direction +
|
|
1485
|
+
* screen-radius math) - for a camera drawing into a viewport of the given
|
|
1486
|
+
* pixel size. {@link update} passes the canvas size (or, while an XR session
|
|
1487
|
+
* presents, the per-eye viewport); {@link renderView} passes its render
|
|
1488
|
+
* target's size.
|
|
1489
|
+
*
|
|
1490
|
+
* @param positionCamera - Camera whose world position seeds the SH view
|
|
1491
|
+
* direction and screen-radius math. Defaults to `camera`; in XR it is the
|
|
1492
|
+
* head, so both eyes shade from one cyclopean point while each still
|
|
1493
|
+
* projects through its own eye.
|
|
1494
|
+
*/
|
|
1495
|
+
writeViewUniforms(e, t, i, s = e) {
|
|
1496
|
+
this.viewport.value.set(t, i);
|
|
1497
|
+
const r = e.projectionMatrix.elements, n = r[5] * i / 2;
|
|
1498
|
+
this.focal.value.set(r[0] * t / 2, n), s.getWorldPosition(this.localCameraPosition.value), this.worldToLocal(this.localCameraPosition.value), this.foveationMode === "frontier" && n > 0 && (this.pixelScaleLimit.value = this.foveationLimitPx / n);
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Self-adjusts {@link foveationLimitPx} so the frontier cut's drawn-splat count
|
|
1502
|
+
* tracks {@link foveationDrawBudget} (Spark's `maxSplats` feedback). Estimates
|
|
1503
|
+
* the drawn count by replaying the cut on a strided sample of the pool, then
|
|
1504
|
+
* coarsens (grows the limit) when over budget and refines (shrinks toward the
|
|
1505
|
+
* finest {@link foveationTargetPx}) when comfortably under. Throttled; a no-op
|
|
1506
|
+
* outside `'frontier'` mode.
|
|
1507
|
+
*
|
|
1508
|
+
* `viewportY` must be the **same** height {@link writeViewUniforms} is given
|
|
1509
|
+
* for this frame - the two share `foveationLimitPx` in pixels and each
|
|
1510
|
+
* converts it with its own `focalY`, so a mismatch (the full drawing buffer
|
|
1511
|
+
* here against a per-eye viewport there) makes the estimator judge a cut the
|
|
1512
|
+
* shader is not applying, and the ratchet runs away toward the maximum.
|
|
1513
|
+
*/
|
|
1514
|
+
adaptFoveationLimit(e, t) {
|
|
1515
|
+
if (this.foveationMode !== "frontier") return;
|
|
1516
|
+
const i = performance.now();
|
|
1517
|
+
if (i - this.lastFoveationAdaptAt < Kt) return;
|
|
1518
|
+
this.lastFoveationAdaptAt = i;
|
|
1519
|
+
const s = e.projectionMatrix.elements[5] * t / 2;
|
|
1520
|
+
if (!(s > 0)) return;
|
|
1521
|
+
const r = this.foveationLimitPx / s;
|
|
1522
|
+
e.getWorldPosition(_), this.worldToLocal(_), ot.copy(e.matrixWorld).invert(), at.multiplyMatrices(ot, this.matrixWorld).premultiply(e.projectionMatrix);
|
|
1523
|
+
const n = at.elements, { covarianceA: a, covarianceB: c, centers: h } = this.backing, l = c.length / 4;
|
|
1524
|
+
let f = 0, x = 0;
|
|
1525
|
+
for (let m = 0; m < l; m += ht) {
|
|
1526
|
+
const S = m * 4, A = c[S + 3];
|
|
1527
|
+
if (A === 0) continue;
|
|
1528
|
+
f++;
|
|
1529
|
+
const R = h[S], y = h[S + 1], M = h[S + 2], I = (n[3] * R + n[7] * y + n[11] * M + n[15]) * 1.2;
|
|
1530
|
+
if (n[2] * R + n[6] * y + n[10] * M + n[14] <= -I) continue;
|
|
1531
|
+
const B = n[0] * R + n[4] * y + n[8] * M + n[12];
|
|
1532
|
+
if (B > I || B < -I) continue;
|
|
1533
|
+
const E = n[1] * R + n[5] * y + n[9] * M + n[13];
|
|
1534
|
+
if (E > I || E < -I) continue;
|
|
1535
|
+
const W = a[S] + a[S + 3] + c[S + 1], L = 2 * Math.sqrt(Math.max(W, 0) / 3), C = A < 0, ut = Math.abs(A), N = R - _.x, X = y - _.y, j = M - _.z, $ = r * Math.sqrt(N * N + X * X + j * j), dt = C ? 0 : L;
|
|
1536
|
+
ut > $ && dt <= $ && x++;
|
|
1537
|
+
}
|
|
1538
|
+
if (f === 0) return;
|
|
1539
|
+
const p = x * ht, w = this.foveationDrawBudget;
|
|
1540
|
+
p > w * 1.1 ? this.foveationLimitPx = Math.min(this.foveationLimitPx * 1.15, Qt) : p < w * 0.7 && (this.foveationLimitPx = Math.max(this.foveationLimitPx / 1.12, this.foveationTargetPx));
|
|
1541
|
+
}
|
|
1542
|
+
/** Records a written row span for GPU upload and the sort-worker mirror. */
|
|
1543
|
+
markRowsWritten(e, t) {
|
|
1544
|
+
var i;
|
|
1545
|
+
this.pendingUploadRows.push({ start: e, count: t }), ((i = this.sorter) == null ? void 0 : i.kind) === "worker" && this.workerDirtyRows.push({ start: e, count: t });
|
|
1546
|
+
}
|
|
1547
|
+
/** Appends one newly active pool range to the packed source-index list. */
|
|
1548
|
+
activateRecord(e) {
|
|
1549
|
+
const t = e.activePrefix ?? e.count;
|
|
1550
|
+
if (t === 0) return;
|
|
1551
|
+
this.queryEpoch++;
|
|
1552
|
+
const i = this.sourceIndexAttribute.array, s = this.getPoolIndexTemplate(), r = this.activeCount;
|
|
1553
|
+
i.set(s.subarray(e.start, e.start + t), r), this.activeSlotByPoolIndex.set(
|
|
1554
|
+
s.subarray(r, r + t),
|
|
1555
|
+
e.start
|
|
1556
|
+
), this.activeCount += t, this.commitActiveListMutation(r, t);
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Removes one active range and backfills its holes from the packed tail.
|
|
1560
|
+
* The common contiguous case uses one native block copy; the fallback
|
|
1561
|
+
* handles a range split by earlier tail backfills in O(record.count).
|
|
1562
|
+
*/
|
|
1563
|
+
deactivateRecord(e) {
|
|
1564
|
+
const t = e.activePrefix ?? e.count;
|
|
1565
|
+
if (t === 0) return;
|
|
1566
|
+
this.queryEpoch++;
|
|
1567
|
+
const i = this.sourceIndexAttribute.array, s = this.activeSlotByPoolIndex[e.start];
|
|
1568
|
+
let r = !0;
|
|
1569
|
+
for (let c = 1; c < t; c++)
|
|
1570
|
+
if (this.activeSlotByPoolIndex[e.start + c] !== s + c) {
|
|
1571
|
+
r = !1;
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
let n = this.activeCount, a = 0;
|
|
1575
|
+
if (r) {
|
|
1576
|
+
const c = this.activeCount - t, h = Math.min(t, Math.max(0, c - s));
|
|
1577
|
+
if (h > 0) {
|
|
1578
|
+
i.copyWithin(s, this.activeCount - h, this.activeCount);
|
|
1579
|
+
for (let l = 0; l < h; l++) {
|
|
1580
|
+
const f = i[s + l];
|
|
1581
|
+
this.activeSlotByPoolIndex[f] = s + l;
|
|
1582
|
+
}
|
|
1583
|
+
n = s, a = s + h;
|
|
1584
|
+
}
|
|
1585
|
+
this.activeCount = c;
|
|
1586
|
+
} else {
|
|
1587
|
+
for (let c = e.start; c < e.start + t; c++) {
|
|
1588
|
+
const h = this.activeSlotByPoolIndex[c], l = this.activeCount - 1;
|
|
1589
|
+
if (h !== l) {
|
|
1590
|
+
const f = i[l];
|
|
1591
|
+
i[h] = f, this.activeSlotByPoolIndex[f] = h, n = Math.min(n, h), a = Math.max(a, h + 1);
|
|
1592
|
+
}
|
|
1593
|
+
this.activeCount--;
|
|
1594
|
+
}
|
|
1595
|
+
a = Math.min(a, this.activeCount);
|
|
1596
|
+
}
|
|
1597
|
+
this.commitActiveListMutation(n, Math.max(0, a - n));
|
|
1598
|
+
}
|
|
1599
|
+
/** Uploads only the changed packed slots and invalidates the current depth order. */
|
|
1600
|
+
commitActiveListMutation(e, t) {
|
|
1601
|
+
var i;
|
|
1602
|
+
if (t > 0 && (z(this.sourceIndexAttribute, e, t), this.sourceIndexAttribute.needsUpdate = !0), ((i = this.sorter) == null ? void 0 : i.kind) === "worker" || this.sorter === null) {
|
|
1603
|
+
const s = this.sourceIndexAttribute.array, r = this.splatIndexAttribute.array;
|
|
1604
|
+
this.drawListSorted ? (r.set(s.subarray(0, this.activeCount)), this.activeCount > 0 && z(this.splatIndexAttribute, 0, this.activeCount), this.splatIndexAttribute.needsUpdate = !0, this.drawListSorted = !1) : t > 0 && (r.set(s.subarray(e, e + t), e), z(this.splatIndexAttribute, e, t), this.splatIndexAttribute.needsUpdate = !0);
|
|
1605
|
+
}
|
|
1606
|
+
this.geometry.instanceCount = this.activeCount, this.activeListVersion++, this.sortScheduler.invalidateContent();
|
|
1607
|
+
}
|
|
1608
|
+
/** Rewrites the active-splat list (pool indices, range by range). */
|
|
1609
|
+
rebuildActiveList() {
|
|
1610
|
+
var s;
|
|
1611
|
+
const e = this.sourceIndexAttribute.array, t = this.getPoolIndexTemplate();
|
|
1612
|
+
let i = 0;
|
|
1613
|
+
for (const r of this.ranges.values()) {
|
|
1614
|
+
if (!r.active) continue;
|
|
1615
|
+
const n = r.activePrefix ?? r.count;
|
|
1616
|
+
e.set(t.subarray(r.start, r.start + n), i), this.activeSlotByPoolIndex.set(t.subarray(i, i + n), r.start), i += n;
|
|
1617
|
+
}
|
|
1618
|
+
this.activeCount = i, this.sourceIndexAttribute.clearUpdateRanges(), i > 0 && this.sourceIndexAttribute.addUpdateRange(0, i), this.sourceIndexAttribute.needsUpdate = !0, (((s = this.sorter) == null ? void 0 : s.kind) === "worker" || this.sorter === null) && (this.splatIndexAttribute.array.set(e.subarray(0, i)), this.splatIndexAttribute.clearUpdateRanges(), i > 0 && this.splatIndexAttribute.addUpdateRange(0, i), this.splatIndexAttribute.needsUpdate = !0, this.drawListSorted = !1), this.geometry.instanceCount = i, this.activeListVersion++, this.sortScheduler.invalidateContent();
|
|
1619
|
+
}
|
|
1620
|
+
/** The pool's reusable index ramp; shared by every mesh drawing from it. */
|
|
1621
|
+
getPoolIndexTemplate() {
|
|
1622
|
+
return this.pool.indexTemplate();
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Uploads rows written since the last flush by copying only those rows
|
|
1626
|
+
* through a staging texture. (Constructor-time writes never appear here;
|
|
1627
|
+
* they ride the textures' initial full upload instead.)
|
|
1628
|
+
*/
|
|
1629
|
+
flushPendingUploads(e) {
|
|
1630
|
+
if (this.pendingUploadRows.length > 0) {
|
|
1631
|
+
const t = this.poolFloatTextures === "float16" ? d.HalfFloatType : d.FloatType;
|
|
1632
|
+
this.uploadRows(e, this.pendingUploadRows, d.RGBAFormat, 4, [
|
|
1633
|
+
{
|
|
1634
|
+
key: "centers",
|
|
1635
|
+
texture: this.dataTextures[0],
|
|
1636
|
+
data: this.backing.centers,
|
|
1637
|
+
type: t,
|
|
1638
|
+
encodeHalf: this.poolFloatTextures === "float16"
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
key: "colors",
|
|
1642
|
+
texture: this.dataTextures[1],
|
|
1643
|
+
data: this.backing.colors,
|
|
1644
|
+
type: d.UnsignedByteType
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
key: "covarianceA",
|
|
1648
|
+
texture: this.dataTextures[2],
|
|
1649
|
+
data: this.backing.covarianceA,
|
|
1650
|
+
type: t,
|
|
1651
|
+
encodeHalf: this.poolFloatTextures === "float16"
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
key: "covarianceB",
|
|
1655
|
+
texture: this.dataTextures[3],
|
|
1656
|
+
data: this.backing.covarianceB,
|
|
1657
|
+
type: d.FloatType
|
|
1658
|
+
}
|
|
1659
|
+
]), this.shPackedTextures.length > 0 && this.uploadRows(
|
|
1660
|
+
e,
|
|
1661
|
+
this.pendingUploadRows,
|
|
1662
|
+
d.RGBAIntegerFormat,
|
|
1663
|
+
4,
|
|
1664
|
+
this.shPackedTextures.map((i, s) => ({
|
|
1665
|
+
key: `shPacked${s}`,
|
|
1666
|
+
texture: i,
|
|
1667
|
+
data: this.backing.shPacked[s],
|
|
1668
|
+
type: d.UnsignedIntType
|
|
1669
|
+
}))
|
|
1670
|
+
), this.pendingUploadRows = [];
|
|
1671
|
+
}
|
|
1672
|
+
for (const [t, i] of this.channels.entries())
|
|
1673
|
+
i.pendingRows.length !== 0 && (this.uploadRows(e, i.pendingRows, d.RedFormat, 1, [
|
|
1674
|
+
{
|
|
1675
|
+
key: `channel:${t}`,
|
|
1676
|
+
texture: i.texture,
|
|
1677
|
+
data: i.backing,
|
|
1678
|
+
type: i.textureType
|
|
1679
|
+
}
|
|
1680
|
+
]), i.pendingRows = []);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Packs float32 CPU backing into the half GPU images for centers/covA.
|
|
1684
|
+
* Used once after a static constructor write so the initial `needsUpdate`
|
|
1685
|
+
* upload carries half bits (staging is skipped for that path).
|
|
1686
|
+
*/
|
|
1687
|
+
syncHalfFloatPoolImages() {
|
|
1688
|
+
const e = this.dataTextures[0], t = this.dataTextures[2], i = e.image.data, s = t.image.data;
|
|
1689
|
+
G(this.backing.centers, i), G(this.backing.covarianceA, s), e.needsUpdate = !0, t.needsUpdate = !0;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Uploads the given row spans of one or more same-layout pool textures via
|
|
1693
|
+
* per-region staging copies. Spans are merged first so consecutive appends
|
|
1694
|
+
* upload as one rectangle each - the copyTextureToTexture call count, not
|
|
1695
|
+
* the pixel volume, dominates.
|
|
1696
|
+
*/
|
|
1697
|
+
uploadRows(e, t, i, s, r) {
|
|
1698
|
+
const n = v.DATA_TEXTURE_WIDTH, a = t.sort((h, l) => h.start - l.start), c = [];
|
|
1699
|
+
for (const h of a) {
|
|
1700
|
+
const l = c[c.length - 1];
|
|
1701
|
+
l && h.start <= l.start + l.count ? l.count = Math.max(l.count, h.start + h.count - l.start) : c.push({ start: h.start, count: h.count });
|
|
1702
|
+
}
|
|
1703
|
+
for (const h of c)
|
|
1704
|
+
for (const { key: l, texture: f, data: x, type: p, encodeHalf: w } of r) {
|
|
1705
|
+
const m = x.subarray(
|
|
1706
|
+
h.start * n * s,
|
|
1707
|
+
(h.start + h.count) * n * s
|
|
1708
|
+
);
|
|
1709
|
+
let S = m;
|
|
1710
|
+
if (w) {
|
|
1711
|
+
const R = this.acquireHalfEncodeBuffer(l, m.length);
|
|
1712
|
+
G(m, R, 0, m.length), S = R;
|
|
1713
|
+
}
|
|
1714
|
+
const A = this.acquireUploadStaging(
|
|
1715
|
+
l,
|
|
1716
|
+
S,
|
|
1717
|
+
n,
|
|
1718
|
+
h.count,
|
|
1719
|
+
i,
|
|
1720
|
+
p
|
|
1721
|
+
);
|
|
1722
|
+
e.copyTextureToTexture(A, f, null, Zt.set(0, h.start));
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
/** Reuses a same-length half encode buffer for one staging key. */
|
|
1726
|
+
acquireHalfEncodeBuffer(e, t) {
|
|
1727
|
+
const i = this.halfEncodeBuffers.get(e);
|
|
1728
|
+
if (i && i.length === t) return i;
|
|
1729
|
+
const s = new Uint16Array(t);
|
|
1730
|
+
return this.halfEncodeBuffers.set(e, s), s;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Reuses one of the recent same-sized staging textures for this upload. WebGPU
|
|
1734
|
+
* texture dimensions are immutable, so each cached entry remains exact-sized. Keeping
|
|
1735
|
+
* a small LRU per channel avoids recreating all core/SH staging resources when
|
|
1736
|
+
* a stream alternates among a few chunk heights, while bounding GPU memory.
|
|
1737
|
+
*/
|
|
1738
|
+
acquireUploadStaging(e, t, i, s, r, n) {
|
|
1739
|
+
var l;
|
|
1740
|
+
let a = this.uploadStaging.get(e);
|
|
1741
|
+
a || (a = /* @__PURE__ */ new Map(), this.uploadStaging.set(e, a));
|
|
1742
|
+
const c = a.get(s);
|
|
1743
|
+
if (c)
|
|
1744
|
+
return c.image = { data: t, width: i, height: s }, c.needsUpdate = !0, a.delete(s), a.set(s, c), c;
|
|
1745
|
+
const h = new d.DataTexture(t, i, s, r, n);
|
|
1746
|
+
if (r === d.RGBAIntegerFormat && (h.magFilter = d.NearestFilter, h.minFilter = d.NearestFilter), h.needsUpdate = !0, a.set(s, h), a.size > Yt) {
|
|
1747
|
+
const f = a.keys().next().value;
|
|
1748
|
+
(l = a.get(f)) == null || l.dispose(), a.delete(f);
|
|
1749
|
+
}
|
|
1750
|
+
return this.updateTimings.stagingTextureAllocations++, h;
|
|
1751
|
+
}
|
|
1752
|
+
/** Gathers everything the material graph reads. See `splat-mesh-material.ts`. */
|
|
1753
|
+
graphInputs(e, t) {
|
|
1754
|
+
return {
|
|
1755
|
+
textures: e,
|
|
1756
|
+
sh: t,
|
|
1757
|
+
sourcePlacement: this.perSourceSort,
|
|
1758
|
+
// The uniform node instances, shared with the pick graph on purpose: one
|
|
1759
|
+
// per-frame write then reaches both.
|
|
1760
|
+
uniforms: {
|
|
1761
|
+
focal: this.focal,
|
|
1762
|
+
viewport: this.viewport,
|
|
1763
|
+
localCameraPosition: this.localCameraPosition,
|
|
1764
|
+
pixelScaleLimit: this.pixelScaleLimit,
|
|
1765
|
+
dofFocusDistance: this.dofFocusDistance,
|
|
1766
|
+
dofAperture: this.dofAperture,
|
|
1767
|
+
screenBandMin: this.screenBandMin,
|
|
1768
|
+
screenBandMax: this.screenBandMax,
|
|
1769
|
+
relightMap: this.relightMap,
|
|
1770
|
+
relightBlend: this.relightBlend,
|
|
1771
|
+
relightBrightness: this.relightBrightness,
|
|
1772
|
+
relightBackground: this.relightBackground,
|
|
1773
|
+
relightSoftness: this.relightSoftness
|
|
1774
|
+
},
|
|
1775
|
+
pick: this.picker.uniforms,
|
|
1776
|
+
settings: {
|
|
1777
|
+
maxStdDev: this.maxStdDev,
|
|
1778
|
+
minSplatSizePx: this.minSplatSizePx,
|
|
1779
|
+
antialias: this.antialias,
|
|
1780
|
+
projectedFilterProfile: this.projectedFilterProfile,
|
|
1781
|
+
srgbOutput: this.srgbOutput,
|
|
1782
|
+
performanceProfile: this.performanceProfileValue,
|
|
1783
|
+
maxScreenRadiusPx: this.maxSplatScreenRadius,
|
|
1784
|
+
minScreenRadiusPx: this.minSplatScreenRadius,
|
|
1785
|
+
foveationMode: this.foveationMode,
|
|
1786
|
+
maxAspect: this.maxSplatAspect,
|
|
1787
|
+
lodAlpha: this.lodAlpha
|
|
1788
|
+
},
|
|
1789
|
+
// The live map: a rebuild after defineChannel must see the new entry.
|
|
1790
|
+
channels: this.channels,
|
|
1791
|
+
modifiers: this.modifierList
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
buildMaterial(e, t) {
|
|
1795
|
+
Z(
|
|
1796
|
+
this.material,
|
|
1797
|
+
"display",
|
|
1798
|
+
this.graphInputs(e, t)
|
|
1799
|
+
), this.picker.rebuildMaterial();
|
|
1800
|
+
}
|
|
1801
|
+
requestSortIfNeeded(e, t) {
|
|
1802
|
+
if (this.deferSortRequestOnce && (this.deferSortRequestOnce = !1, this.activeListVersion === this.sortedActiveListVersion && !this.orderIsForeign) || this.activeCount === 0) return;
|
|
1803
|
+
this.currentModelView.multiplyMatrices(e.matrixWorldInverse, this.matrixWorld);
|
|
1804
|
+
const i = t.backend.isWebGPUBackend === !0, s = i ? performance.now() : 0;
|
|
1805
|
+
if (!this.orderIsForeign) {
|
|
1806
|
+
if (i) {
|
|
1807
|
+
if (!this.sortScheduler.shouldSubmit(
|
|
1808
|
+
this.currentModelView,
|
|
1809
|
+
this.lastSortedModelView,
|
|
1810
|
+
this.activeCount,
|
|
1811
|
+
s
|
|
1812
|
+
))
|
|
1813
|
+
return;
|
|
1814
|
+
} else if (
|
|
1815
|
+
// WebGL2 has no cadence, but a content swap under a stationary camera
|
|
1816
|
+
// still invalidated the draw order (the swap reset it to unsorted
|
|
1817
|
+
// active order) - skipping here would leave the scene blend-order
|
|
1818
|
+
// broken until the camera next moved.
|
|
1819
|
+
!this.sortScheduler.hasPendingForce() && this.currentModelView.equals(this.lastSortedModelView)
|
|
1820
|
+
)
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
this.refreshSortBounds(), this.sorter ?? (this.sorter = this.createSorter(t)), this.sorter && this.sorter.sort(this.currentModelView, this.activeCount, this.boundingSphereLocal) && (this.lastSortedModelView.copy(this.currentModelView), this.sortedActiveListVersion = this.activeListVersion, this.orderIsForeign = !1, this.sortScheduler.markAccepted(s));
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Refreshes {@link boundingSphereLocal} - the sphere the sorter quantizes
|
|
1827
|
+
* depth over - when {@link boundsDirty}. The base mesh uses its local splat
|
|
1828
|
+
* bounds; a unified {@link MergedSplatMesh} overrides this to supply a world-space
|
|
1829
|
+
* bound spanning all its sources (whose transforms live in the shader).
|
|
1830
|
+
*/
|
|
1831
|
+
refreshSortBounds() {
|
|
1832
|
+
this.boundsDirty && (this.localBounds.getBoundingSphere(this.boundingSphereLocal), this.boundsDirty = !1);
|
|
1833
|
+
}
|
|
1834
|
+
/** The data texture backing a channel defined with {@link defineChannel}. */
|
|
1835
|
+
channelTexture(e) {
|
|
1836
|
+
var t;
|
|
1837
|
+
return (t = this.channels.get(e)) == null ? void 0 : t.texture;
|
|
1838
|
+
}
|
|
1839
|
+
/** CPU backing storage of a float channel, for internal CPU-side consumers. */
|
|
1840
|
+
channelBacking(e) {
|
|
1841
|
+
const t = this.channels.get(e);
|
|
1842
|
+
if (!t || !(t.backing instanceof Float32Array))
|
|
1843
|
+
throw new Error(`SplatMesh: float channel "${e}" is missing.`);
|
|
1844
|
+
return t.backing;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Forces the next {@link update} to re-sort even if the camera has not moved
|
|
1848
|
+
* - used by a unified pool when a source's transform changes, since the depth
|
|
1849
|
+
* order then changes without any camera motion.
|
|
1850
|
+
*/
|
|
1851
|
+
invalidateSort() {
|
|
1852
|
+
this.sortScheduler.invalidate(), this.boundsDirty = !0;
|
|
1853
|
+
}
|
|
1854
|
+
createSorter(e) {
|
|
1855
|
+
if (!(e.backend.isWebGPUBackend === !0))
|
|
1856
|
+
return new qt({
|
|
1857
|
+
capacity: this.capacity,
|
|
1858
|
+
rowWidth: v.DATA_TEXTURE_WIDTH,
|
|
1859
|
+
centers: this.backing.centers,
|
|
1860
|
+
perSource: this.perSourceSort ? {
|
|
1861
|
+
sourceIds: this.perSourceSort.sourceIds,
|
|
1862
|
+
matrices: this.perSourceSort.matrices
|
|
1863
|
+
} : void 0,
|
|
1864
|
+
splatIndexAttribute: this.splatIndexAttribute,
|
|
1865
|
+
takeDirtyRows: () => {
|
|
1866
|
+
const s = this.workerDirtyRows;
|
|
1867
|
+
return this.workerDirtyRows = [], s;
|
|
1868
|
+
},
|
|
1869
|
+
getActiveSpans: () => {
|
|
1870
|
+
const s = new Uint32Array(this.ranges.size * 2);
|
|
1871
|
+
let r = 0;
|
|
1872
|
+
for (const n of this.ranges.values()) {
|
|
1873
|
+
if (!n.active) continue;
|
|
1874
|
+
const a = n.activePrefix ?? n.count;
|
|
1875
|
+
a !== 0 && (s[r++] = n.start, s[r++] = a);
|
|
1876
|
+
}
|
|
1877
|
+
return s.subarray(0, r);
|
|
1878
|
+
},
|
|
1879
|
+
onOrderApplied: () => {
|
|
1880
|
+
this.drawListSorted = !0;
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
const i = {
|
|
1884
|
+
renderer: e,
|
|
1885
|
+
capacity: this.poolRows * v.DATA_TEXTURE_WIDTH,
|
|
1886
|
+
centersTexture: this.centersTexture,
|
|
1887
|
+
dataTextureWidth: v.DATA_TEXTURE_WIDTH,
|
|
1888
|
+
splatIndexAttribute: this.splatIndexAttribute,
|
|
1889
|
+
sourceIndexAttribute: this.sourceIndexAttribute
|
|
1890
|
+
};
|
|
1891
|
+
return this.perSourceSort ? new tt({ ...i, perSource: this.perSourceSort }) : this.sortStrategy === "radix" || this.sortStrategy === "exact" ? (this.ensureRadixSorter(), this.RadixSorterCtor ? new this.RadixSorterCtor({ ...i, exactDepth: this.sortStrategy === "exact" }) : null) : new tt(i);
|
|
1892
|
+
}
|
|
1893
|
+
/** Prefetches the experimental radix sorter; safe to call repeatedly. */
|
|
1894
|
+
ensureRadixSorter() {
|
|
1895
|
+
this.RadixSorterCtor || this.radixSorterLoad || (this.radixSorterLoad = import("./radix-sorter-R0FCSqKu.js").then((e) => {
|
|
1896
|
+
this.RadixSorterCtor = e.RadixSorter;
|
|
1897
|
+
}).finally(() => {
|
|
1898
|
+
this.radixSorterLoad = null;
|
|
1899
|
+
}));
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
o(v, "DATA_TEXTURE_WIDTH", xt);
|
|
1903
|
+
let nt = v;
|
|
1904
|
+
const F = new d.Box3(), Zt = new d.Vector2(), T = new d.Vector3(), k = new d.Vector3(), q = new d.Vector3(), P = new d.Vector2(), _ = new d.Vector3(), ot = new d.Matrix4(), at = new d.Matrix4(), Kt = 180, ht = 31, Qt = 64;
|
|
1905
|
+
function ct(g) {
|
|
1906
|
+
if (g !== void 0 && (!Number.isFinite(g) || g <= 0))
|
|
1907
|
+
throw new RangeError("SplatMesh maxStdDev must be a finite number greater than 0.");
|
|
1908
|
+
return g;
|
|
1909
|
+
}
|
|
1910
|
+
function Jt(g) {
|
|
1911
|
+
if (g !== void 0 && (!Number.isFinite(g) || g < 0))
|
|
1912
|
+
throw new RangeError("SplatMesh minSplatSizePx must be a finite number >= 0.");
|
|
1913
|
+
return g;
|
|
1914
|
+
}
|
|
1915
|
+
function H(g) {
|
|
1916
|
+
if (g === void 0) return 0;
|
|
1917
|
+
if (!Number.isFinite(g) || g < 0)
|
|
1918
|
+
throw new RangeError("SplatMesh screen-radius cull must be a finite number ≥ 0.");
|
|
1919
|
+
return g;
|
|
1920
|
+
}
|
|
1921
|
+
const te = 1;
|
|
1922
|
+
function ee(g) {
|
|
1923
|
+
if (g === void 0) return te;
|
|
1924
|
+
if (!Number.isFinite(g) || g <= 0)
|
|
1925
|
+
throw new RangeError("SplatMesh foveationTargetPx must be a finite number > 0.");
|
|
1926
|
+
return g;
|
|
1927
|
+
}
|
|
1928
|
+
const ie = 9e5;
|
|
1929
|
+
function se(g) {
|
|
1930
|
+
if (g === void 0) return ie;
|
|
1931
|
+
if (!Number.isFinite(g) || g <= 0)
|
|
1932
|
+
throw new RangeError("SplatMesh foveationDrawBudget must be a finite number > 0.");
|
|
1933
|
+
return g;
|
|
1934
|
+
}
|
|
1935
|
+
export {
|
|
1936
|
+
ie as D,
|
|
1937
|
+
nt as S,
|
|
1938
|
+
te as a,
|
|
1939
|
+
Ft as c,
|
|
1940
|
+
Vt as y
|
|
1941
|
+
};
|
|
1942
|
+
//# sourceMappingURL=splat-mesh-aQdcWA6v.js.map
|