dylanebert HF staff commited on
Commit
bdef08e
·
1 Parent(s): ebb3bda

first pass gaussian splatting viewer

Browse files
viewer/src/lib/data/scenes.json CHANGED
@@ -177,7 +177,7 @@
177
  "model": "3dgs",
178
  "title": "Stump",
179
  "type": "splat",
180
- "url": "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/stump/point_cloud/iteration_7000/point_cloud.ply",
181
  "pipeline": [
182
  "Gaussian Splatting"
183
  ]
 
177
  "model": "3dgs",
178
  "title": "Stump",
179
  "type": "splat",
180
+ "url": "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/stump/stump-7k.splat",
181
  "pipeline": [
182
  "Gaussian Splatting"
183
  ]
viewer/src/routes/viewer/[slug]/+page.svelte CHANGED
@@ -51,6 +51,7 @@
51
 
52
  function destroyViewer() {
53
  document.body.classList.remove("viewer");
 
54
  }
55
 
56
  function handleMobileView() {
 
51
 
52
  function destroyViewer() {
53
  document.body.classList.remove("viewer");
54
+ viewer.dispose();
55
  }
56
 
57
  function handleMobileView() {
viewer/src/routes/viewer/[slug]/BabylonViewer.ts CHANGED
@@ -33,9 +33,12 @@ export class BabylonViewer implements IViewer {
33
  this.camera.panningSensibility = 10000 / this.camera.radius;
34
  });
35
 
36
- window.addEventListener("resize", () => {
37
- this.engine.resize();
38
- });
 
 
 
39
  }
40
 
41
  async loadModel(url: string, loadingBarCallback?: (progress: number) => void) {
@@ -108,6 +111,10 @@ export class BabylonViewer implements IViewer {
108
  if (this.scene) {
109
  this.scene.dispose();
110
  }
 
 
 
 
111
  }
112
 
113
  async capture(): Promise<string | null> {
 
33
  this.camera.panningSensibility = 10000 / this.camera.radius;
34
  });
35
 
36
+ this.handleResize = this.handleResize.bind(this);
37
+ window.addEventListener("resize", this.handleResize);
38
+ }
39
+
40
+ handleResize() {
41
+ this.engine.resize();
42
  }
43
 
44
  async loadModel(url: string, loadingBarCallback?: (progress: number) => void) {
 
111
  if (this.scene) {
112
  this.scene.dispose();
113
  }
114
+ if (this.engine) {
115
+ this.engine.dispose();
116
+ }
117
+ window.removeEventListener("resize", this.handleResize);
118
  }
119
 
120
  async capture(): Promise<string | null> {
viewer/src/routes/viewer/[slug]/SplatViewer.ts CHANGED
@@ -1,38 +1,943 @@
1
  import type { IViewer } from "./IViewer";
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  export class SplatViewer implements IViewer {
4
  canvas: HTMLCanvasElement;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  constructor(canvas: HTMLCanvasElement) {
 
7
  this.canvas = canvas;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
  async loadModel(url: string, loadingBarCallback?: (progress: number) => void) {
12
- this.draw();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
- draw = () => {
16
- if (!this.canvas) return;
17
- const ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
18
- ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
19
 
20
- const text = "Under Construction";
21
- ctx.font = '16px sans-serif';
22
- ctx.fillStyle = '#fff';
23
- ctx.textAlign = 'center';
24
- ctx.textBaseline = 'middle';
25
 
26
- const centerX = this.canvas.width / 2;
27
- const centerY = this.canvas.height / 2;
 
 
 
 
 
28
 
29
- ctx.fillText(text, centerX, centerY);
 
 
30
 
31
- requestAnimationFrame(this.draw);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
 
34
  dispose() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
 
36
  }
37
 
38
  async capture() {
 
1
  import type { IViewer } from "./IViewer";
2
 
3
+ class OrbitCamera {
4
+ position: number[];
5
+ rotation: number[];
6
+ fy: number;
7
+ fx: number;
8
+ alpha: number;
9
+ beta: number;
10
+ radius: number;
11
+ target: number[];
12
+ desiredAlpha: number;
13
+ desiredBeta: number;
14
+ desiredRadius: number;
15
+ desiredTarget: number[];
16
+ damping: number;
17
+
18
+ minBeta = 5 * Math.PI / 180;
19
+ maxBeta = 85 * Math.PI / 180;
20
+ minZoom = 0.5;
21
+ maxZoom = 30;
22
+ orbitSpeed = 0.005;
23
+ panSpeed = 0.01;
24
+ zoomSpeed = 0.05;
25
+
26
+ constructor(alpha = 0, beta = 0, radius = 5, target = [0, 0, 0]) {
27
+ this.position = [0, 0, -radius];
28
+ this.rotation = [
29
+ [1, 0, 0],
30
+ [0, 1, 0],
31
+ [0, 0, 1],
32
+ ].flat();
33
+ this.fx = 1132;
34
+ this.fy = 1132;
35
+ this.alpha = alpha;
36
+ this.beta = beta;
37
+ this.radius = radius;
38
+ this.target = target;
39
+ this.desiredAlpha = alpha;
40
+ this.desiredBeta = beta;
41
+ this.desiredRadius = radius;
42
+ this.desiredTarget = target;
43
+ this.damping = 0.4;
44
+ this.updateCameraPositionAndRotation();
45
+ }
46
+
47
+ lerp(start: number, end: number, factor: number) {
48
+ return (1 - factor) * start + factor * end;
49
+ }
50
+
51
+ pan(dx: number, dy: number) {
52
+ const right = [this.rotation[0], this.rotation[3], this.rotation[6]];
53
+ const up = [this.rotation[1], this.rotation[4], this.rotation[7]];
54
+ for (let i = 0; i < 3; i++) {
55
+ this.desiredTarget[i] += right[i] * dx + up[i] * dy;
56
+ }
57
+ }
58
+
59
+ getZoomNorm(): number {
60
+ return 0.1 + 0.9 * (this.desiredRadius - this.minZoom) / (this.maxZoom - this.minZoom);
61
+ }
62
+
63
+ update() {
64
+ this.alpha = this.lerp(this.alpha, this.desiredAlpha, this.damping);
65
+ this.beta = this.lerp(this.beta, this.desiredBeta, this.damping);
66
+ this.radius = this.lerp(this.radius, this.desiredRadius, this.damping);
67
+ for (let i = 0; i < 3; i++) {
68
+ this.target[i] = this.lerp(this.target[i], this.desiredTarget[i], this.damping);
69
+ }
70
+ this.updateCameraPositionAndRotation();
71
+ }
72
+
73
+ updateCameraPositionAndRotation() {
74
+ const x = this.target[0] + this.radius * Math.sin(this.alpha) * Math.cos(this.beta);
75
+ const y = this.target[1] - this.radius * Math.sin(this.beta);
76
+ const z = this.target[2] - this.radius * Math.cos(this.alpha) * Math.cos(this.beta);
77
+ this.position = [x, y, z];
78
+
79
+ const direction = normalize(subtract(this.target, this.position));
80
+ const eulerAngles = directionToEuler(direction);
81
+ this.rotation = eulerToRotationMatrix(eulerAngles[0], eulerAngles[1], eulerAngles[2]);
82
+ }
83
+ }
84
+
85
+ function directionToEuler(direction: number[]) {
86
+ const x = Math.asin(-direction[1]);
87
+ const y = Math.atan2(direction[0], direction[2]);
88
+ return [x, y, 0];
89
+ }
90
+
91
+ function eulerToRotationMatrix(x: number, y: number, z: number) {
92
+ const cx = Math.cos(x);
93
+ const sx = Math.sin(x);
94
+ const cy = Math.cos(y);
95
+ const sy = Math.sin(y);
96
+ const cz = Math.cos(z);
97
+ const sz = Math.sin(z);
98
+
99
+ const rotationMatrix = [
100
+ cy * cz + sy * sx * sz, -cy * sz + sy * sx * cz, sy * cx,
101
+ cx * sz, cx * cz, -sx,
102
+ -sy * cz + cy * sx * sz, sy * sz + cy * sx * cz, cy * cx,
103
+ ]
104
+
105
+ return rotationMatrix;
106
+ }
107
+
108
+ function normalize(a: number[]) {
109
+ const len = Math.hypot(...a);
110
+ return a.map((v) => v / len);
111
+ }
112
+
113
+ function subtract(a: number[], b: number[]) {
114
+ return a.map((v, i) => v - b[i]);
115
+ }
116
+
117
+ function getProjectionMatrix(fx: number, fy: number, width: number, height: number) {
118
+ const znear = 0.2;
119
+ const zfar = 200;
120
+ return [
121
+ [(2 * fx) / width, 0, 0, 0],
122
+ [0, -(2 * fy) / height, 0, 0],
123
+ [0, 0, zfar / (zfar - znear), 1],
124
+ [0, 0, -(zfar * znear) / (zfar - znear), 0],
125
+ ].flat();
126
+ }
127
+
128
+ function getViewMatrix(camera: any) {
129
+ const R = camera.rotation.flat();
130
+ const t = camera.position;
131
+ const camToWorld = [
132
+ [R[0], R[1], R[2], 0],
133
+ [R[3], R[4], R[5], 0],
134
+ [R[6], R[7], R[8], 0],
135
+ [
136
+ -t[0] * R[0] - t[1] * R[3] - t[2] * R[6],
137
+ -t[0] * R[1] - t[1] * R[4] - t[2] * R[7],
138
+ -t[0] * R[2] - t[1] * R[5] - t[2] * R[8],
139
+ 1,
140
+ ],
141
+ ].flat();
142
+ return camToWorld;
143
+ }
144
+
145
+ function multiply4(a: number[], b: number[]) {
146
+ return [
147
+ b[0] * a[0] + b[1] * a[4] + b[2] * a[8] + b[3] * a[12],
148
+ b[0] * a[1] + b[1] * a[5] + b[2] * a[9] + b[3] * a[13],
149
+ b[0] * a[2] + b[1] * a[6] + b[2] * a[10] + b[3] * a[14],
150
+ b[0] * a[3] + b[1] * a[7] + b[2] * a[11] + b[3] * a[15],
151
+ b[4] * a[0] + b[5] * a[4] + b[6] * a[8] + b[7] * a[12],
152
+ b[4] * a[1] + b[5] * a[5] + b[6] * a[9] + b[7] * a[13],
153
+ b[4] * a[2] + b[5] * a[6] + b[6] * a[10] + b[7] * a[14],
154
+ b[4] * a[3] + b[5] * a[7] + b[6] * a[11] + b[7] * a[15],
155
+ b[8] * a[0] + b[9] * a[4] + b[10] * a[8] + b[11] * a[12],
156
+ b[8] * a[1] + b[9] * a[5] + b[10] * a[9] + b[11] * a[13],
157
+ b[8] * a[2] + b[9] * a[6] + b[10] * a[10] + b[11] * a[14],
158
+ b[8] * a[3] + b[9] * a[7] + b[10] * a[11] + b[11] * a[15],
159
+ b[12] * a[0] + b[13] * a[4] + b[14] * a[8] + b[15] * a[12],
160
+ b[12] * a[1] + b[13] * a[5] + b[14] * a[9] + b[15] * a[13],
161
+ b[12] * a[2] + b[13] * a[6] + b[14] * a[10] + b[15] * a[14],
162
+ b[12] * a[3] + b[13] * a[7] + b[14] * a[11] + b[15] * a[15],
163
+ ];
164
+ }
165
+
166
+ function createWorker(self: Worker) {
167
+ let buffer: ArrayBuffer;
168
+ let vertexCount = 0;
169
+ let viewProj: Float32Array;
170
+ // 6*4 + 4 + 4 = 8*4
171
+ // XYZ - Position (Float32)
172
+ // XYZ - Scale (Float32)
173
+ // RGBA - colors (uint8)
174
+ // IJKL - quaternion/rot (uint8)
175
+ const rowLength = 3 * 4 + 3 * 4 + 4 + 4;
176
+ let depthIndex = new Uint32Array();
177
+
178
+ function processPlyBuffer(inputBuffer: ArrayBuffer) {
179
+ const ubuf = new Uint8Array(inputBuffer);
180
+ // 10KB ought to be enough for a header...
181
+ const header = new TextDecoder().decode(ubuf.slice(0, 1024 * 10));
182
+ const header_end = "end_header\n";
183
+ const header_end_index = header.indexOf(header_end);
184
+ if (header_end_index < 0)
185
+ throw new Error("Unable to read .ply file header");
186
+ const vertexCount = parseInt(/element vertex (\d+)\n/.exec(header)![1]);
187
+ console.log("Vertex Count", vertexCount);
188
+ let row_offset: number = 0;
189
+ let offsets: { [key: string]: number } = {};
190
+ let types: { [key: string]: string } = {};
191
+ const TYPE_MAP: { [key: string]: string } = {
192
+ double: "getFloat64",
193
+ int: "getInt32",
194
+ uint: "getUint32",
195
+ float: "getFloat32",
196
+ short: "getInt16",
197
+ ushort: "getUint16",
198
+ uchar: "getUint8",
199
+ };
200
+ for (let prop of header
201
+ .slice(0, header_end_index)
202
+ .split("\n")
203
+ .filter((k) => k.startsWith("property "))) {
204
+ const [_p, type, name] = prop.split(" ");
205
+ const arrayType = TYPE_MAP[type] || "getInt8";
206
+ types[name] = arrayType;
207
+ offsets[name] = row_offset;
208
+ row_offset += parseInt(arrayType.replace(/[^\d]/g, "")) / 8;
209
+ }
210
+
211
+ let dataView = new DataView(
212
+ inputBuffer,
213
+ header_end_index + header_end.length,
214
+ );
215
+ let row: number = 0;
216
+ const attrs: any = new Proxy(
217
+ {},
218
+ {
219
+ get(_target, prop: string) {
220
+ if (!types[prop]) throw new Error(prop + " not found");
221
+ const type = types[prop] as keyof DataView;
222
+ const dataViewMethod = dataView[type] as any;
223
+ return dataViewMethod(
224
+ row * row_offset + offsets[prop],
225
+ true,
226
+ );
227
+ },
228
+ },
229
+ );
230
+
231
+ // 6*4 + 4 + 4 = 8*4
232
+ // XYZ - Position (Float32)
233
+ // XYZ - Scale (Float32)
234
+ // RGBA - colors (uint8)
235
+ // IJKL - quaternion/rot (uint8)
236
+ const rowLength = 3 * 4 + 3 * 4 + 4 + 4;
237
+ const buffer = new ArrayBuffer(rowLength * vertexCount);
238
+
239
+ for (let j = 0; j < vertexCount; j++) {
240
+ row = j;
241
+
242
+ const position = new Float32Array(buffer, j * rowLength, 3);
243
+ const scales = new Float32Array(buffer, j * rowLength + 4 * 3, 3);
244
+ const rgba = new Uint8ClampedArray(
245
+ buffer,
246
+ j * rowLength + 4 * 3 + 4 * 3,
247
+ 4,
248
+ );
249
+ const rot = new Uint8ClampedArray(
250
+ buffer,
251
+ j * rowLength + 4 * 3 + 4 * 3 + 4,
252
+ 4,
253
+ );
254
+
255
+ if (types["scale_0"]) {
256
+ const qlen = Math.sqrt(
257
+ attrs.rot_0 ** 2 +
258
+ attrs.rot_1 ** 2 +
259
+ attrs.rot_2 ** 2 +
260
+ attrs.rot_3 ** 2,
261
+ );
262
+
263
+ rot[0] = (attrs.rot_0 / qlen) * 128 + 128;
264
+ rot[1] = (attrs.rot_1 / qlen) * 128 + 128;
265
+ rot[2] = (attrs.rot_2 / qlen) * 128 + 128;
266
+ rot[3] = (attrs.rot_3 / qlen) * 128 + 128;
267
+
268
+ scales[0] = Math.exp(attrs.scale_0);
269
+ scales[1] = Math.exp(attrs.scale_1);
270
+ scales[2] = Math.exp(attrs.scale_2);
271
+ } else {
272
+ scales[0] = 0.01;
273
+ scales[1] = 0.01;
274
+ scales[2] = 0.01;
275
+
276
+ rot[0] = 255;
277
+ rot[1] = 0;
278
+ rot[2] = 0;
279
+ rot[3] = 0;
280
+ }
281
+
282
+ position[0] = attrs.x;
283
+ position[1] = attrs.y;
284
+ position[2] = attrs.z;
285
+
286
+ if (types["f_dc_0"]) {
287
+ const SH_C0 = 0.28209479177387814;
288
+ rgba[0] = (0.5 + SH_C0 * attrs.f_dc_0) * 255;
289
+ rgba[1] = (0.5 + SH_C0 * attrs.f_dc_1) * 255;
290
+ rgba[2] = (0.5 + SH_C0 * attrs.f_dc_2) * 255;
291
+ } else {
292
+ rgba[0] = attrs.red;
293
+ rgba[1] = attrs.green;
294
+ rgba[2] = attrs.blue;
295
+ }
296
+ if (types["opacity"]) {
297
+ rgba[3] = (1 / (1 + Math.exp(-attrs.opacity))) * 255;
298
+ } else {
299
+ rgba[3] = 255;
300
+ }
301
+ }
302
+ return buffer;
303
+ }
304
+
305
+ const runSort = (viewProj: Float32Array) => {
306
+ if (!buffer) return;
307
+
308
+ const f_buffer = new Float32Array(buffer);
309
+ const u_buffer = new Uint8Array(buffer);
310
+
311
+ const covA = new Float32Array(3 * vertexCount);
312
+ const covB = new Float32Array(3 * vertexCount);
313
+
314
+ const center = new Float32Array(3 * vertexCount);
315
+ const color = new Float32Array(4 * vertexCount);
316
+
317
+ let maxDepth = -Infinity;
318
+ let minDepth = Infinity;
319
+ let sizeList = new Int32Array(vertexCount);
320
+ for (let i = 0; i < vertexCount; i++) {
321
+ let depth =
322
+ ((viewProj[2] * f_buffer[8 * i + 0] +
323
+ viewProj[6] * f_buffer[8 * i + 1] +
324
+ viewProj[10] * f_buffer[8 * i + 2]) *
325
+ 4096) |
326
+ 0;
327
+ sizeList[i] = depth;
328
+ if (depth > maxDepth) maxDepth = depth;
329
+ if (depth < minDepth) minDepth = depth;
330
+ }
331
+
332
+ // This is a 16 bit single-pass counting sort
333
+ let depthInv = (256 * 256) / (maxDepth - minDepth);
334
+ let counts0 = new Uint32Array(256 * 256);
335
+ for (let i = 0; i < vertexCount; i++) {
336
+ sizeList[i] = ((sizeList[i] - minDepth) * depthInv) | 0;
337
+ counts0[sizeList[i]]++;
338
+ }
339
+ let starts0 = new Uint32Array(256 * 256);
340
+ for (let i = 1; i < 256 * 256; i++) starts0[i] = starts0[i - 1] + counts0[i - 1];
341
+ depthIndex = new Uint32Array(vertexCount);
342
+ for (let i = 0; i < vertexCount; i++) depthIndex[starts0[sizeList[i]]++] = i;
343
+
344
+
345
+ for (let j = 0; j < vertexCount; j++) {
346
+ const i = depthIndex[j];
347
+
348
+ center[3 * j + 0] = f_buffer[8 * i + 0];
349
+ center[3 * j + 1] = f_buffer[8 * i + 1];
350
+ center[3 * j + 2] = f_buffer[8 * i + 2];
351
+
352
+ color[4 * j + 0] = u_buffer[32 * i + 24 + 0] / 255;
353
+ color[4 * j + 1] = u_buffer[32 * i + 24 + 1] / 255;
354
+ color[4 * j + 2] = u_buffer[32 * i + 24 + 2] / 255;
355
+ color[4 * j + 3] = u_buffer[32 * i + 24 + 3] / 255;
356
+
357
+ let scale = [
358
+ f_buffer[8 * i + 3 + 0],
359
+ f_buffer[8 * i + 3 + 1],
360
+ f_buffer[8 * i + 3 + 2],
361
+ ];
362
+ let rot = [
363
+ (u_buffer[32 * i + 28 + 0] - 128) / 128,
364
+ (u_buffer[32 * i + 28 + 1] - 128) / 128,
365
+ (u_buffer[32 * i + 28 + 2] - 128) / 128,
366
+ (u_buffer[32 * i + 28 + 3] - 128) / 128,
367
+ ];
368
+
369
+ const R = [
370
+ 1.0 - 2.0 * (rot[2] * rot[2] + rot[3] * rot[3]),
371
+ 2.0 * (rot[1] * rot[2] + rot[0] * rot[3]),
372
+ 2.0 * (rot[1] * rot[3] - rot[0] * rot[2]),
373
+
374
+ 2.0 * (rot[1] * rot[2] - rot[0] * rot[3]),
375
+ 1.0 - 2.0 * (rot[1] * rot[1] + rot[3] * rot[3]),
376
+ 2.0 * (rot[2] * rot[3] + rot[0] * rot[1]),
377
+
378
+ 2.0 * (rot[1] * rot[3] + rot[0] * rot[2]),
379
+ 2.0 * (rot[2] * rot[3] - rot[0] * rot[1]),
380
+ 1.0 - 2.0 * (rot[1] * rot[1] + rot[2] * rot[2]),
381
+ ];
382
+
383
+ // Compute the matrix product of S and R (M = S * R)
384
+ const M = [
385
+ scale[0] * R[0],
386
+ scale[0] * R[1],
387
+ scale[0] * R[2],
388
+ scale[1] * R[3],
389
+ scale[1] * R[4],
390
+ scale[1] * R[5],
391
+ scale[2] * R[6],
392
+ scale[2] * R[7],
393
+ scale[2] * R[8],
394
+ ];
395
+
396
+ covA[3 * j + 0] = M[0] * M[0] + M[3] * M[3] + M[6] * M[6];
397
+ covA[3 * j + 1] = M[0] * M[1] + M[3] * M[4] + M[6] * M[7];
398
+ covA[3 * j + 2] = M[0] * M[2] + M[3] * M[5] + M[6] * M[8];
399
+ covB[3 * j + 0] = M[1] * M[1] + M[4] * M[4] + M[7] * M[7];
400
+ covB[3 * j + 1] = M[1] * M[2] + M[4] * M[5] + M[7] * M[8];
401
+ covB[3 * j + 2] = M[2] * M[2] + M[5] * M[5] + M[8] * M[8];
402
+ }
403
+
404
+ self.postMessage({ covA, center, color, covB, viewProj }, [
405
+ covA.buffer,
406
+ center.buffer,
407
+ color.buffer,
408
+ covB.buffer,
409
+ ]);
410
+ };
411
+
412
+ const throttledSort = () => {
413
+ if (!sortRunning) {
414
+ sortRunning = true;
415
+ let lastView = viewProj;
416
+ runSort(lastView);
417
+ setTimeout(() => {
418
+ sortRunning = false;
419
+ if (lastView !== viewProj) {
420
+ throttledSort();
421
+ }
422
+ }, 0);
423
+ }
424
+ };
425
+
426
+ let sortRunning: boolean = false;
427
+ self.onmessage = (e) => {
428
+ if (e.data.ply) {
429
+ vertexCount = 0;
430
+ runSort(viewProj);
431
+ buffer = processPlyBuffer(e.data.ply);
432
+ vertexCount = Math.floor(buffer.byteLength / rowLength);
433
+ postMessage({ buffer: buffer });
434
+ } else if (e.data.buffer) {
435
+ buffer = e.data.buffer;
436
+ vertexCount = e.data.vertexCount;
437
+ } else if (e.data.vertexCount) {
438
+ vertexCount = e.data.vertexCount;
439
+ } else if (e.data.view) {
440
+ viewProj = e.data.view;
441
+ throttledSort();
442
+ }
443
+ };
444
+ }
445
+
446
+ function preventDefault(e: Event) {
447
+ e.preventDefault();
448
+ e.stopPropagation();
449
+ };
450
+
451
+ const vertexShaderSource = `
452
+ precision mediump float;
453
+ attribute vec2 position;
454
+
455
+ attribute vec4 color;
456
+ attribute vec3 center;
457
+ attribute vec3 covA;
458
+ attribute vec3 covB;
459
+
460
+ uniform mat4 projection, view;
461
+ uniform vec2 focal;
462
+ uniform vec2 viewport;
463
+
464
+ varying vec4 vColor;
465
+ varying vec2 vPosition;
466
+
467
+ mat3 transpose(mat3 m) {
468
+ return mat3(
469
+ m[0][0], m[1][0], m[2][0],
470
+ m[0][1], m[1][1], m[2][1],
471
+ m[0][2], m[1][2], m[2][2]
472
+ );
473
+ }
474
+
475
+ void main () {
476
+ vec4 camspace = view * vec4(center, 1);
477
+ vec4 pos2d = projection * camspace;
478
+
479
+ float bounds = 1.2 * pos2d.w;
480
+ if (pos2d.z < -pos2d.w || pos2d.x < -bounds || pos2d.x > bounds
481
+ || pos2d.y < -bounds || pos2d.y > bounds) {
482
+ gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
483
+ return;
484
+ }
485
+
486
+ mat3 Vrk = mat3(
487
+ covA.x, covA.y, covA.z,
488
+ covA.y, covB.x, covB.y,
489
+ covA.z, covB.y, covB.z
490
+ );
491
+
492
+ mat3 J = mat3(
493
+ focal.x / camspace.z, 0., -(focal.x * camspace.x) / (camspace.z * camspace.z),
494
+ 0., -focal.y / camspace.z, (focal.y * camspace.y) / (camspace.z * camspace.z),
495
+ 0., 0., 0.
496
+ );
497
+
498
+ mat3 W = transpose(mat3(view));
499
+ mat3 T = W * J;
500
+ mat3 cov = transpose(T) * Vrk * T;
501
+
502
+ vec2 vCenter = vec2(pos2d) / pos2d.w;
503
+
504
+ float diagonal1 = cov[0][0] + 0.3;
505
+ float offDiagonal = cov[0][1];
506
+ float diagonal2 = cov[1][1] + 0.3;
507
+
508
+ float mid = 0.5 * (diagonal1 + diagonal2);
509
+ float radius = length(vec2((diagonal1 - diagonal2) / 2.0, offDiagonal));
510
+ float lambda1 = mid + radius;
511
+ float lambda2 = max(mid - radius, 0.1);
512
+ vec2 diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1));
513
+ vec2 v1 = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector;
514
+ vec2 v2 = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x);
515
+
516
+ vColor = color;
517
+ vPosition = position;
518
+
519
+ gl_Position = vec4(
520
+ vCenter
521
+ + position.x * v1 / viewport * 2.0
522
+ + position.y * v2 / viewport * 2.0, 0.0, 1.0
523
+ );
524
+ }
525
+ `;
526
+
527
+ const fragmentShaderSource = `
528
+ precision mediump float;
529
+
530
+ varying vec4 vColor;
531
+ varying vec2 vPosition;
532
+
533
+ void main () {
534
+ float A = -dot(vPosition, vPosition);
535
+ if (A < -4.0) discard;
536
+ float B = exp(A) * vColor.a;
537
+ gl_FragColor = vec4(B * vColor.rgb, B);
538
+ }
539
+ `;
540
+
541
  export class SplatViewer implements IViewer {
542
  canvas: HTMLCanvasElement;
543
+ gl: WebGLRenderingContext;
544
+ ext: ANGLE_instanced_arrays;
545
+ camera: OrbitCamera;
546
+
547
+ splatData: Uint8Array;
548
+ vertexCount: number;
549
+ worker: Worker;
550
+
551
+ vertexShader: WebGLShader;
552
+ fragmentShader: WebGLShader;
553
+ program: WebGLProgram;
554
+ a_position: number;
555
+ a_center: number;
556
+ a_color: number;
557
+ a_covA: number;
558
+ a_covB: number;
559
+ vertexBuffer: WebGLBuffer | null;
560
+ centerBuffer: WebGLBuffer | null;
561
+ colorBuffer: WebGLBuffer | null;
562
+ covABuffer: WebGLBuffer | null;
563
+ covBBuffer: WebGLBuffer | null;
564
+
565
+ dragging = false;
566
+ panning = false;
567
+ lastX: number = 0;
568
+ lastY: number = 0;
569
+
570
+ animationFrameId: number | null = null;
571
+ disposed: boolean = false;
572
 
573
  constructor(canvas: HTMLCanvasElement) {
574
+ this.disposed = false;
575
  this.canvas = canvas;
576
+ this.gl = this.initWebGL();
577
+ this.ext = this.gl.getExtension("ANGLE_instanced_arrays") as ANGLE_instanced_arrays;
578
+ this.camera = new OrbitCamera();
579
+
580
+ this.splatData = new Uint8Array();
581
+ this.vertexCount = 0;
582
+
583
+ this.worker = new Worker(
584
+ URL.createObjectURL(
585
+ new Blob(["(", createWorker.toString(), ")(self)"], {
586
+ type: "application/javascript",
587
+ }),
588
+ ),
589
+ );
590
+
591
+ this.canvas.width = innerWidth;
592
+ this.canvas.height = innerHeight;
593
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
594
+
595
+ let projectionMatrix = getProjectionMatrix(
596
+ this.camera.fx,
597
+ this.camera.fy,
598
+ this.canvas.width,
599
+ this.canvas.height,
600
+ );
601
+
602
+ let viewMatrix = getViewMatrix(this.camera);
603
+
604
+ this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER) as WebGLShader;
605
+ this.gl.shaderSource(this.vertexShader, vertexShaderSource);
606
+ this.gl.compileShader(this.vertexShader);
607
+ if (!this.gl.getShaderParameter(this.vertexShader, this.gl.COMPILE_STATUS)) {
608
+ console.error(this.gl.getShaderInfoLog(this.vertexShader));
609
+ }
610
+
611
+ this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER) as WebGLShader;
612
+ this.gl.shaderSource(this.fragmentShader, fragmentShaderSource);
613
+ this.gl.compileShader(this.fragmentShader);
614
+ if (!this.gl.getShaderParameter(this.fragmentShader, this.gl.COMPILE_STATUS)) {
615
+ console.error(this.gl.getShaderInfoLog(this.fragmentShader));
616
+ }
617
+
618
+ this.program = this.gl.createProgram() as WebGLProgram;
619
+ this.gl.attachShader(this.program, this.vertexShader);
620
+ this.gl.attachShader(this.program, this.fragmentShader);
621
+ this.gl.linkProgram(this.program);
622
+ this.gl.useProgram(this.program);
623
+
624
+ if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
625
+ console.error(this.gl.getProgramInfoLog(this.program));
626
+ }
627
+
628
+ this.gl.disable(this.gl.DEPTH_TEST); // Disable depth testing
629
+
630
+ // Enable blending
631
+ this.gl.enable(this.gl.BLEND);
632
+
633
+ // Set blending function
634
+ this.gl.blendFuncSeparate(
635
+ this.gl.ONE_MINUS_DST_ALPHA,
636
+ this.gl.ONE,
637
+ this.gl.ONE_MINUS_DST_ALPHA,
638
+ this.gl.ONE,
639
+ );
640
+
641
+ // Set blending equation
642
+ this.gl.blendEquationSeparate(this.gl.FUNC_ADD, this.gl.FUNC_ADD);
643
+
644
+ // projection
645
+ const u_projection = this.gl.getUniformLocation(this.program, "projection");
646
+ this.gl.uniformMatrix4fv(u_projection, false, projectionMatrix);
647
+
648
+ // viewport
649
+ const u_viewport = this.gl.getUniformLocation(this.program, "viewport");
650
+ this.gl.uniform2fv(u_viewport, new Float32Array([this.canvas.width, this.canvas.height]));
651
+
652
+ // focal
653
+ const u_focal = this.gl.getUniformLocation(this.program, "focal");
654
+ this.gl.uniform2fv(
655
+ u_focal,
656
+ new Float32Array([this.camera.fx, this.camera.fy]),
657
+ );
658
+
659
+ // view
660
+ const u_view = this.gl.getUniformLocation(this.program, "view");
661
+ this.gl.uniformMatrix4fv(u_view, false, viewMatrix);
662
+
663
+ // positions
664
+ const triangleVertices = new Float32Array([-2, -2, 2, -2, 2, 2, -2, 2]);
665
+ this.vertexBuffer = this.gl.createBuffer();
666
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
667
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, triangleVertices, this.gl.STATIC_DRAW);
668
+ this.a_position = this.gl.getAttribLocation(this.program, "position");
669
+ this.gl.enableVertexAttribArray(this.a_position);
670
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
671
+ this.gl.vertexAttribPointer(this.a_position, 2, this.gl.FLOAT, false, 0, 0);
672
+
673
+ // center
674
+ this.centerBuffer = this.gl.createBuffer();
675
+ this.a_center = this.gl.getAttribLocation(this.program, "center");
676
+ this.gl.enableVertexAttribArray(this.a_center);
677
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.centerBuffer);
678
+ this.gl.vertexAttribPointer(this.a_center, 3, this.gl.FLOAT, false, 0, 0);
679
+ this.ext.vertexAttribDivisorANGLE(this.a_center, 1); // Use the extension here
680
+
681
+ // color
682
+ this.colorBuffer = this.gl.createBuffer();
683
+ this.a_color = this.gl.getAttribLocation(this.program, "color");
684
+ this.gl.enableVertexAttribArray(this.a_color);
685
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.colorBuffer);
686
+ this.gl.vertexAttribPointer(this.a_color, 4, this.gl.FLOAT, false, 0, 0);
687
+ this.ext.vertexAttribDivisorANGLE(this.a_color, 1); // Use the extension here
688
+
689
+ // cov
690
+ this.covABuffer = this.gl.createBuffer();
691
+ this.a_covA = this.gl.getAttribLocation(this.program, "covA");
692
+ this.gl.enableVertexAttribArray(this.a_covA);
693
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.covABuffer);
694
+ this.gl.vertexAttribPointer(this.a_covA, 3, this.gl.FLOAT, false, 0, 0);
695
+ this.ext.vertexAttribDivisorANGLE(this.a_covA, 1); // Use the extension here
696
+
697
+ this.covBBuffer = this.gl.createBuffer();
698
+ this.a_covB = this.gl.getAttribLocation(this.program, "covB");
699
+ this.gl.enableVertexAttribArray(this.a_covB);
700
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.covBBuffer);
701
+ this.gl.vertexAttribPointer(this.a_covB, 3, this.gl.FLOAT, false, 0, 0);
702
+ this.ext.vertexAttribDivisorANGLE(this.a_covB, 1); // Use the extension here
703
+
704
+ this.worker.onmessage = (e) => {
705
+ if (e.data.buffer) {
706
+ this.splatData = new Uint8Array(e.data.buffer);
707
+ const blob = new Blob([this.splatData.buffer], {
708
+ type: "application/octet-stream",
709
+ });
710
+ const link = document.createElement("a");
711
+ link.download = "model.splat";
712
+ link.href = URL.createObjectURL(blob);
713
+ document.body.appendChild(link);
714
+ link.click();
715
+ } else {
716
+ let { covA, covB, center, color } = e.data;
717
+ vertexCount = center.length / 3;
718
+
719
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.centerBuffer);
720
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, center, this.gl.DYNAMIC_DRAW);
721
+
722
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.colorBuffer);
723
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, color, this.gl.DYNAMIC_DRAW);
724
+
725
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.covABuffer);
726
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, covA, this.gl.DYNAMIC_DRAW);
727
+
728
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.covBBuffer);
729
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, covB, this.gl.DYNAMIC_DRAW);
730
+ }
731
+ };
732
+
733
+ let vertexCount = 0;
734
+
735
+ const frame = () => {
736
+ this.camera.update();
737
+ viewMatrix = getViewMatrix(this.camera);
738
 
739
+ const viewProj = multiply4(projectionMatrix, viewMatrix);
740
+ this.worker.postMessage({ view: viewProj });
741
+
742
+ if (vertexCount > 0) {
743
+ this.gl.uniformMatrix4fv(u_view, false, viewMatrix);
744
+ this.ext.drawArraysInstancedANGLE(this.gl.TRIANGLE_FAN, 0, 4, vertexCount);
745
+ } else {
746
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
747
+ }
748
+
749
+ if (!this.disposed) {
750
+ this.animationFrameId = requestAnimationFrame(frame);
751
+ }
752
+ };
753
+
754
+ this.animationFrameId = requestAnimationFrame(frame);
755
+
756
+ document.addEventListener("dragenter", preventDefault);
757
+ document.addEventListener("dragover", preventDefault);
758
+ document.addEventListener("dragleave", preventDefault);
759
+ document.addEventListener("contextmenu", preventDefault);
760
+
761
+ this.handleMouseDown = this.handleMouseDown.bind(this);
762
+ this.handleMouseUp = this.handleMouseUp.bind(this);
763
+ this.handleMouseMove = this.handleMouseMove.bind(this);
764
+ this.handleMouseWheel = this.handleMouseWheel.bind(this);
765
+ this.handleDrop = this.handleDrop.bind(this);
766
+ this.handleResize = this.handleResize.bind(this);
767
+
768
+ this.canvas.addEventListener("mousedown", this.handleMouseDown);
769
+ this.canvas.addEventListener("mouseup", this.handleMouseUp);
770
+ this.canvas.addEventListener("mousemove", this.handleMouseMove);
771
+ this.canvas.addEventListener("wheel", this.handleMouseWheel);
772
+ this.canvas.addEventListener("drop", this.handleDrop);
773
+
774
+ window.addEventListener("resize", this.handleResize);
775
+ }
776
+
777
+ initWebGL(): WebGLRenderingContext {
778
+ const gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
779
+ if (!gl) {
780
+ alert('WebGL is not supported on your browser!');
781
+ }
782
+ return gl as WebGLRenderingContext;
783
  }
784
 
785
  async loadModel(url: string, loadingBarCallback?: (progress: number) => void) {
786
+ const req = await fetch(url, {
787
+ mode: "cors",
788
+ credentials: "omit",
789
+ });
790
+ console.log(req);
791
+ if (req.status != 200) {
792
+ throw new Error(req.status + " Unable to load " + req.url);
793
+ }
794
+
795
+ const rowLength = 3 * 4 + 3 * 4 + 4 + 4;
796
+ const reader = req.body!.getReader();
797
+ this.splatData = new Uint8Array(req.headers.get("content-length") as any);
798
+
799
+ console.log("Vertex Count", this.splatData.length / rowLength);
800
+
801
+ let bytesRead = 0;
802
+ let lastVertexCount = -1;
803
+ let stopLoading = false;
804
+
805
+ while (true) {
806
+ const { done, value } = await reader.read();
807
+ if (done || stopLoading) break;
808
+
809
+ this.splatData.set(value, bytesRead);
810
+ bytesRead += value.length;
811
+
812
+ if (loadingBarCallback) {
813
+ loadingBarCallback(bytesRead / this.splatData.length);
814
+ }
815
+
816
+ if (this.vertexCount > lastVertexCount) {
817
+ this.worker.postMessage({
818
+ buffer: this.splatData.buffer,
819
+ vertexCount: Math.floor(bytesRead / rowLength),
820
+ });
821
+ lastVertexCount = this.vertexCount;
822
+ }
823
+ }
824
+ if (!stopLoading) {
825
+ this.worker.postMessage({
826
+ buffer: this.splatData.buffer,
827
+ vertexCount: Math.floor(bytesRead / rowLength),
828
+ });
829
+ }
830
+ }
831
+
832
+ handleMouseDown(e: MouseEvent) {
833
+ this.dragging = true;
834
+ this.panning = e.button === 2;
835
+ this.lastX = e.clientX;
836
+ this.lastY = e.clientY;
837
  }
838
 
839
+ handleMouseUp(e: MouseEvent) {
840
+ this.dragging = false;
841
+ this.panning = false;
842
+ }
843
 
844
+ handleMouseMove(e: MouseEvent) {
845
+ if (!this.dragging) return;
846
+ const dx = e.clientX - this.lastX;
847
+ const dy = e.clientY - this.lastY;
848
+ const zoomNorm = this.camera.getZoomNorm();
849
 
850
+ if (this.panning) {
851
+ this.camera.pan(-dx * this.camera.panSpeed * zoomNorm, -dy * this.camera.panSpeed * zoomNorm);
852
+ } else {
853
+ this.camera.desiredAlpha -= dx * this.camera.orbitSpeed;
854
+ this.camera.desiredBeta += dy * this.camera.orbitSpeed;
855
+ this.camera.desiredBeta = Math.min(Math.max(this.camera.desiredBeta, this.camera.minBeta), this.camera.maxBeta);
856
+ }
857
 
858
+ this.lastX = e.clientX;
859
+ this.lastY = e.clientY;
860
+ }
861
 
862
+ handleMouseWheel(e: WheelEvent) {
863
+ const zoomNorm = this.camera.getZoomNorm();
864
+ this.camera.desiredRadius += e.deltaY * this.camera.zoomSpeed * zoomNorm;
865
+ this.camera.desiredRadius = Math.min(Math.max(this.camera.desiredRadius, this.camera.minZoom), this.camera.maxZoom);
866
+ }
867
+
868
+ handleDrop(e: DragEvent) {
869
+ e.preventDefault();
870
+ e.stopPropagation();
871
+ this.selectFile(e.dataTransfer!.files[0]);
872
+ }
873
+
874
+ selectFile(file: File) {
875
+ const reader = new FileReader();
876
+ reader.onload = () => {
877
+ this.splatData = new Uint8Array(reader.result as ArrayBuffer);
878
+ if (this.splatData[0] !== 112 || this.splatData[1] !== 108 || this.splatData[2] !== 121 || this.splatData[3] !== 10) {
879
+ alert("Invalid file format");
880
+ return;
881
+ }
882
+ this.worker.postMessage({ ply: this.splatData.buffer });
883
+ };
884
+ reader.readAsArrayBuffer(file);
885
+ }
886
+
887
+ handleResize() {
888
+ this.canvas.width = innerWidth;
889
+ this.canvas.height = innerHeight;
890
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
891
  }
892
 
893
  dispose() {
894
+ this.worker.terminate();
895
+
896
+ this.gl.disableVertexAttribArray(this.a_position);
897
+ this.gl.disableVertexAttribArray(this.a_center);
898
+ this.gl.disableVertexAttribArray(this.a_color);
899
+ this.gl.disableVertexAttribArray(this.a_covA);
900
+ this.gl.disableVertexAttribArray(this.a_covB);
901
+
902
+ this.gl.deleteBuffer(this.vertexBuffer);
903
+ this.gl.deleteBuffer(this.centerBuffer);
904
+ this.gl.deleteBuffer(this.colorBuffer);
905
+ this.gl.deleteBuffer(this.covABuffer);
906
+ this.gl.deleteBuffer(this.covBBuffer);
907
+
908
+ this.gl.detachShader(this.program, this.vertexShader);
909
+ this.gl.detachShader(this.program, this.fragmentShader);
910
+ this.gl.deleteShader(this.vertexShader);
911
+ this.gl.deleteShader(this.fragmentShader);
912
+
913
+ this.gl.deleteProgram(this.program);
914
+
915
+ this.vertexBuffer = null;
916
+ this.centerBuffer = null;
917
+ this.colorBuffer = null;
918
+ this.covABuffer = null;
919
+ this.covBBuffer = null;
920
+
921
+ this.splatData = new Uint8Array();
922
+
923
+ if (this.animationFrameId) {
924
+ cancelAnimationFrame(this.animationFrameId);
925
+ }
926
+
927
+ this.disposed = true;
928
+
929
+ document.removeEventListener("dragenter", preventDefault);
930
+ document.removeEventListener("dragover", preventDefault);
931
+ document.removeEventListener("dragleave", preventDefault);
932
+ document.removeEventListener("contextmenu", preventDefault);
933
+
934
+ this.canvas.removeEventListener("mousedown", this.handleMouseDown);
935
+ this.canvas.removeEventListener("mouseup", this.handleMouseUp);
936
+ this.canvas.removeEventListener("mousemove", this.handleMouseMove);
937
+ this.canvas.removeEventListener("wheel", this.handleMouseWheel);
938
+ this.canvas.removeEventListener("drop", this.handleDrop);
939
 
940
+ window.removeEventListener("resize", this.handleResize);
941
  }
942
 
943
  async capture() {