dylanebert HF staff commited on
Commit
3b7903d
·
1 Parent(s): 4e1da8c

formatting

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