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

in progress splat viewer

Browse files
viewer/src/lib/data/models.json CHANGED
@@ -21,5 +21,13 @@
21
  "project": "https://mv-dream.github.io/",
22
  "code": "https://github.com/bytedance/MVDream",
23
  "spaces": []
 
 
 
 
 
 
 
 
24
  }
25
  ]
 
21
  "project": "https://mv-dream.github.io/",
22
  "code": "https://github.com/bytedance/MVDream",
23
  "spaces": []
24
+ },
25
+ {
26
+ "slug": "3dgs",
27
+ "title": "3D Gaussian Splatting",
28
+ "paper": "https://huggingface.co/papers/2308.04079",
29
+ "project": "https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/",
30
+ "code": "https://github.com/graphdeco-inria/gaussian-splatting",
31
+ "spaces": []
32
  }
33
  ]
viewer/src/lib/data/scenes.json CHANGED
@@ -3,6 +3,7 @@
3
  "slug": "sync-dreamer-armor",
4
  "model": "sync-dreamer",
5
  "title": "armor",
 
6
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/armor.glb",
7
  "pipeline": [
8
  "Multi-view Diffusion",
@@ -13,6 +14,7 @@
13
  "slug": "sync-dreamer-deer",
14
  "model": "sync-dreamer",
15
  "title": "deer",
 
16
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/deer.glb",
17
  "pipeline": [
18
  "Multi-view Diffusion",
@@ -23,6 +25,7 @@
23
  "slug": "sync-dreamer-drum",
24
  "model": "sync-dreamer",
25
  "title": "drum",
 
26
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/drum.glb",
27
  "pipeline": [
28
  "Multi-view Diffusion",
@@ -33,6 +36,7 @@
33
  "slug": "sync-dreamer-forest",
34
  "model": "sync-dreamer",
35
  "title": "forest",
 
36
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/forest.glb",
37
  "pipeline": [
38
  "Multi-view Diffusion",
@@ -43,6 +47,7 @@
43
  "slug": "sync-dreamer-monkey",
44
  "model": "sync-dreamer",
45
  "title": "monkey",
 
46
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/monkey.glb",
47
  "pipeline": [
48
  "Multi-view Diffusion",
@@ -53,6 +58,7 @@
53
  "slug": "sync-dreamer-poro",
54
  "model": "sync-dreamer",
55
  "title": "poro",
 
56
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/poro.glb",
57
  "pipeline": [
58
  "Multi-view Diffusion",
@@ -63,6 +69,7 @@
63
  "slug": "sync-dreamer-train",
64
  "model": "sync-dreamer",
65
  "title": "train",
 
66
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/train.glb",
67
  "pipeline": [
68
  "Multi-view Diffusion",
@@ -73,6 +80,7 @@
73
  "slug": "dreamfusion-sweaterfrog",
74
  "model": "dreamfusion",
75
  "title": "sweater frog",
 
76
  "url": "https://dreamfusion3d.github.io/assets/meshes2/sweaterfrog_1step.glb",
77
  "prompt": "frog wearing a sweater",
78
  "pipeline": [
@@ -84,6 +92,7 @@
84
  "slug": "dreamfusion-chick",
85
  "model": "dreamfusion",
86
  "title": "chick",
 
87
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44855521_sept18_hero16_047a_DSLR_photo_of_an_eggshell_broken_in_two_with_an_adorable_chick_standing_next_to_it_1step.glb",
88
  "prompt": "eggshell broken in two with an adorable chick standing next to it",
89
  "pipeline": [
@@ -95,6 +104,7 @@
95
  "slug": "dreamfusion-ghost",
96
  "model": "dreamfusion",
97
  "title": "ghost",
 
98
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44934035_sept18_hero19_113a_DSLR_photo_of_a_ghost_eating_a_hamburger_1step.glb",
99
  "prompt": "ghost eating a hamburger",
100
  "pipeline": [
@@ -106,6 +116,7 @@
106
  "slug": "dreamfusion-pig",
107
  "model": "dreamfusion",
108
  "title": "pig",
 
109
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44844973_sept18_hero14_076a_pig_wearing_a_backpack_1step.glb",
110
  "prompt": "a pig wearing a backback",
111
  "pipeline": [
@@ -117,6 +128,7 @@
117
  "slug": "dreamfusion-eagle",
118
  "model": "dreamfusion",
119
  "title": "eagle",
 
120
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44853505_sept18_hero15_145a_bald_eagle_carved_out_of_wood_1step.glb",
121
  "prompt": "a bald eagle carved out of wood",
122
  "pipeline": [
@@ -128,6 +140,7 @@
128
  "slug": "dreamfusion-crab",
129
  "model": "dreamfusion",
130
  "title": "crab",
 
131
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44930695_sept18_hero18_103a_crab,_low_poly_1step.glb",
132
  "prompt": "a crab, low poly",
133
  "pipeline": [
@@ -139,6 +152,7 @@
139
  "slug": "dreamfusion-lemur",
140
  "model": "dreamfusion",
141
  "title": "lemur",
 
142
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44853505_sept18_hero15_124a_lemur_taking_notes_in_a_journal_1step.glb",
143
  "prompt": "a lemur taking notes in a journal",
144
  "pipeline": [
@@ -150,11 +164,22 @@
150
  "slug": "dreamfusion-corgi",
151
  "model": "dreamfusion",
152
  "title": "corgi",
 
153
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44960400_sept18_hero20peter_117a_plush_toy_of_a_corgi_nurse_1step.glb",
154
  "prompt": "a plush toy of a corgi nurse",
155
  "pipeline": [
156
  "Multi-view Diffusion",
157
  "Marching Cubes"
158
  ]
 
 
 
 
 
 
 
 
 
 
159
  }
160
  ]
 
3
  "slug": "sync-dreamer-armor",
4
  "model": "sync-dreamer",
5
  "title": "armor",
6
+ "type": "mesh",
7
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/armor.glb",
8
  "pipeline": [
9
  "Multi-view Diffusion",
 
14
  "slug": "sync-dreamer-deer",
15
  "model": "sync-dreamer",
16
  "title": "deer",
17
+ "type": "mesh",
18
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/deer.glb",
19
  "pipeline": [
20
  "Multi-view Diffusion",
 
25
  "slug": "sync-dreamer-drum",
26
  "model": "sync-dreamer",
27
  "title": "drum",
28
+ "type": "mesh",
29
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/drum.glb",
30
  "pipeline": [
31
  "Multi-view Diffusion",
 
36
  "slug": "sync-dreamer-forest",
37
  "model": "sync-dreamer",
38
  "title": "forest",
39
+ "type": "mesh",
40
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/forest.glb",
41
  "pipeline": [
42
  "Multi-view Diffusion",
 
47
  "slug": "sync-dreamer-monkey",
48
  "model": "sync-dreamer",
49
  "title": "monkey",
50
+ "type": "mesh",
51
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/monkey.glb",
52
  "pipeline": [
53
  "Multi-view Diffusion",
 
58
  "slug": "sync-dreamer-poro",
59
  "model": "sync-dreamer",
60
  "title": "poro",
61
+ "type": "mesh",
62
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/poro.glb",
63
  "pipeline": [
64
  "Multi-view Diffusion",
 
69
  "slug": "sync-dreamer-train",
70
  "model": "sync-dreamer",
71
  "title": "train",
72
+ "type": "mesh",
73
  "url": "https://huggingface.co/datasets/dylanebert/igf-results/resolve/main/sync-dreamer/train.glb",
74
  "pipeline": [
75
  "Multi-view Diffusion",
 
80
  "slug": "dreamfusion-sweaterfrog",
81
  "model": "dreamfusion",
82
  "title": "sweater frog",
83
+ "type": "mesh",
84
  "url": "https://dreamfusion3d.github.io/assets/meshes2/sweaterfrog_1step.glb",
85
  "prompt": "frog wearing a sweater",
86
  "pipeline": [
 
92
  "slug": "dreamfusion-chick",
93
  "model": "dreamfusion",
94
  "title": "chick",
95
+ "type": "mesh",
96
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44855521_sept18_hero16_047a_DSLR_photo_of_an_eggshell_broken_in_two_with_an_adorable_chick_standing_next_to_it_1step.glb",
97
  "prompt": "eggshell broken in two with an adorable chick standing next to it",
98
  "pipeline": [
 
104
  "slug": "dreamfusion-ghost",
105
  "model": "dreamfusion",
106
  "title": "ghost",
107
+ "type": "mesh",
108
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44934035_sept18_hero19_113a_DSLR_photo_of_a_ghost_eating_a_hamburger_1step.glb",
109
  "prompt": "ghost eating a hamburger",
110
  "pipeline": [
 
116
  "slug": "dreamfusion-pig",
117
  "model": "dreamfusion",
118
  "title": "pig",
119
+ "type": "mesh",
120
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44844973_sept18_hero14_076a_pig_wearing_a_backpack_1step.glb",
121
  "prompt": "a pig wearing a backback",
122
  "pipeline": [
 
128
  "slug": "dreamfusion-eagle",
129
  "model": "dreamfusion",
130
  "title": "eagle",
131
+ "type": "mesh",
132
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44853505_sept18_hero15_145a_bald_eagle_carved_out_of_wood_1step.glb",
133
  "prompt": "a bald eagle carved out of wood",
134
  "pipeline": [
 
140
  "slug": "dreamfusion-crab",
141
  "model": "dreamfusion",
142
  "title": "crab",
143
+ "type": "mesh",
144
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44930695_sept18_hero18_103a_crab,_low_poly_1step.glb",
145
  "prompt": "a crab, low poly",
146
  "pipeline": [
 
152
  "slug": "dreamfusion-lemur",
153
  "model": "dreamfusion",
154
  "title": "lemur",
155
+ "type": "mesh",
156
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44853505_sept18_hero15_124a_lemur_taking_notes_in_a_journal_1step.glb",
157
  "prompt": "a lemur taking notes in a journal",
158
  "pipeline": [
 
164
  "slug": "dreamfusion-corgi",
165
  "model": "dreamfusion",
166
  "title": "corgi",
167
+ "type": "mesh",
168
  "url": "https://dreamfusion3d.github.io/assets/meshes2/44960400_sept18_hero20peter_117a_plush_toy_of_a_corgi_nurse_1step.glb",
169
  "prompt": "a plush toy of a corgi nurse",
170
  "pipeline": [
171
  "Multi-view Diffusion",
172
  "Marching Cubes"
173
  ]
174
+ },
175
+ {
176
+ "slug": "3dgs-stump",
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
+ ]
184
  }
185
  ]
viewer/src/routes/viewer/[slug]/+page.svelte CHANGED
@@ -1,13 +1,14 @@
1
  <script lang="ts">
2
  import { onMount, onDestroy } from "svelte";
3
- import * as BABYLON from "@babylonjs/core";
4
- import "@babylonjs/loaders/glTF";
5
- import "@babylonjs/loaders/OBJ";
6
 
7
  export let data: {
8
  scene: {
9
  title: string;
10
  model: string;
 
11
  url: string;
12
  prompt: string;
13
  pipeline: string[];
@@ -17,166 +18,81 @@
17
  };
18
  };
19
 
20
- let overlay: HTMLDivElement | null = null;
21
- let hud: HTMLDivElement | null = null;
22
- let hudToggleBtn: HTMLButtonElement | null = null;
23
- let triangleCount: HTMLSpanElement | null = null;
24
- let fpsCount: HTMLSpanElement | null = null;
25
- let engine: BABYLON.Engine | null = null;
26
- let scene: BABYLON.Scene | null = null;
27
- let camera: BABYLON.ArcRotateCamera | null = null;
28
- let container: HTMLDivElement | null = null;
29
- let canvas: HTMLCanvasElement | null = null;
30
- let loadingBarFill: HTMLDivElement | null = null;
31
- let collapsed = false;
32
-
33
- onMount(() => {
34
- document.body.classList.add("viewer");
35
 
36
- const isMobile = window.innerWidth < 768;
37
- if (isMobile) {
38
- collapsed = true;
39
- hudToggleBtn!.textContent = ")";
40
- container!.classList.remove("hud-expanded");
41
- }
42
-
43
- loadModel(data.scene.url);
44
 
45
- hudToggleBtn!.addEventListener("click", () => {
46
- collapsed = !collapsed;
47
- hudToggleBtn!.textContent = collapsed ? ")" : "(";
48
- if (collapsed) {
49
- container!.classList.remove("hud-expanded");
50
- } else {
51
- container!.classList.add("hud-expanded");
52
- }
53
- });
54
 
55
- const modeItems = document.querySelectorAll(".mode-item");
56
- modeItems.forEach((item) => {
57
- item.addEventListener("click", (event) => {
58
- const currentTarget = event.currentTarget as HTMLElement;
59
- const mode = currentTarget.getAttribute("data-mode");
60
-
61
- modeItems.forEach((i) => i.classList.remove("active"));
62
- currentTarget.classList.add("active");
63
-
64
- switch (mode) {
65
- case "wireframe":
66
- scene!.forceWireframe = true;
67
- break;
68
- default:
69
- scene!.forceWireframe = false;
70
- break;
71
- }
72
- });
73
- });
74
- });
75
-
76
- onDestroy(() => {
77
- if (scene) {
78
- scene.dispose();
79
- document.body.classList.remove("viewer");
80
  }
81
- });
82
-
83
- async function loadModel(url: string) {
84
- overlay!.style.display = "flex";
85
-
86
- engine = new BABYLON.Engine(canvas, true);
87
-
88
- scene = new BABYLON.Scene(engine);
89
- scene.clearColor = BABYLON.Color4.FromHexString("#1A1B1EFF");
90
-
91
- await BABYLON.SceneLoader.AppendAsync("", url, scene, (event) => {
92
- const progress = event.loaded / event.total;
93
- loadingBarFill!.style.width = `${progress * 100}%`;
94
- });
95
-
96
- scene.cameras.forEach((camera) => {
97
- camera.dispose();
98
- });
99
-
100
- scene.lights.forEach((light) => {
101
- light.dispose();
102
- });
103
-
104
- camera = new BABYLON.ArcRotateCamera("camera", Math.PI / 3, Math.PI / 3, 30, BABYLON.Vector3.Zero(), scene);
105
- camera.angularSensibilityY = 1000;
106
- camera.panningSensibility = 500;
107
- camera.wheelPrecision = 5;
108
- camera.inertia = 0.9;
109
- camera.panningInertia = 0.9;
110
- camera.lowerRadiusLimit = 3;
111
- camera.upperRadiusLimit = 100;
112
- camera.setTarget(BABYLON.Vector3.Zero());
113
- camera.attachControl(canvas, true);
114
-
115
- camera.onAfterCheckInputsObservable.add(() => {
116
- camera!.wheelPrecision = 150 / camera!.radius;
117
- camera!.panningSensibility = 10000 / camera!.radius;
118
  });
 
 
 
119
 
120
- const light = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), scene);
121
- light.intensity = 1;
122
- light.diffuse = new BABYLON.Color3(1, 1, 1);
123
- light.groundColor = new BABYLON.Color3(0.3, 0.3, 0.3);
124
-
125
- const sun = new BABYLON.DirectionalLight("sun", new BABYLON.Vector3(-0.5, -1, -0.5), scene);
126
- sun.intensity = 2;
127
- sun.diffuse = new BABYLON.Color3(1, 1, 1);
128
-
129
- const standardSize = 10;
130
- let scaleFactor = 1;
131
- let center = BABYLON.Vector3.Zero();
132
-
133
- if (scene.meshes.length > 0) {
134
- let bounds = scene.meshes[0].getBoundingInfo().boundingBox;
135
- let min = bounds.minimumWorld;
136
- let max = bounds.maximumWorld;
137
-
138
- for (let i = 1; i < scene.meshes.length; i++) {
139
- bounds = scene.meshes[i].getBoundingInfo().boundingBox;
140
- min = BABYLON.Vector3.Minimize(min, bounds.minimumWorld);
141
- max = BABYLON.Vector3.Maximize(max, bounds.maximumWorld);
142
- }
143
-
144
- const extent = max.subtract(min).scale(0.5);
145
- const size = extent.length();
146
 
147
- center = BABYLON.Vector3.Center(min, max);
 
 
 
148
 
149
- scaleFactor = standardSize / size;
 
 
 
 
 
 
150
  }
 
151
 
152
- const parentNode = new BABYLON.TransformNode("parent", scene);
 
 
 
 
 
153
 
154
- let totalTriangles = 0;
155
- scene.meshes.forEach((mesh) => {
156
- mesh.setParent(parentNode);
157
- if (mesh.getTotalVertices() > 0) {
158
- totalTriangles += mesh.getTotalIndices() / 3;
159
- }
160
  });
161
- triangleCount!.textContent = totalTriangles.toLocaleString();
162
 
163
- parentNode.position = center.scale(-1 * scaleFactor);
164
- parentNode.scaling.scaleInPlace(scaleFactor);
165
 
166
- engine.runRenderLoop(() => {
167
- scene!.render();
168
- if (fpsCount) {
169
- fpsCount.textContent = engine!.getFps().toFixed();
170
- }
171
- });
172
 
173
- window.addEventListener("resize", () => {
174
- updateCanvasSize();
175
- engine!.resize();
 
176
  });
177
-
178
  updateCanvasSize();
179
- overlay!.style.display = "none";
180
  }
181
 
182
  function updateCanvasSize() {
@@ -185,17 +101,20 @@
185
  canvas.height = container.clientHeight;
186
  }
187
 
188
- function capture() {
189
- if (!engine || !camera) return;
190
- const cachedColor = scene!.clearColor;
191
- scene!.clearColor = BABYLON.Color4.FromHexString("#00000000");
192
- BABYLON.Tools.CreateScreenshotUsingRenderTarget(engine, camera, 512, (data) => {
193
- const a = document.createElement("a");
194
- a.href = data;
195
- a.download = "screenshot.png";
196
- a.click();
197
- });
198
- scene!.clearColor = cachedColor;
 
 
 
199
  }
200
 
201
  function exit() {
@@ -211,8 +130,8 @@
211
  </div>
212
  <canvas bind:this={canvas} width="512" height="512" />
213
  <div class="exit-button" on:pointerdown={exit}>x</div>
214
- <div bind:this={hud} class="hud" class:collapsed>
215
- <button bind:this={hudToggleBtn} class="hud-toggle-btn">(</button>
216
  <div class="section">
217
  <div class="title">{data.scene.title}</div>
218
  </div>
@@ -221,43 +140,45 @@
221
  <div class="info-panel">
222
  {#if data.scene.model}
223
  <a href={`/models/${data.scene.model}`} class="section-label">{data.model.title}</a>
 
 
 
 
 
 
 
224
  {:else}
225
  <div class="section-label">None</div>
226
  {/if}
227
  </div>
228
  </div>
229
- {#if data.scene.pipeline}
230
  <div class="section">
231
- <div class="section-title">Pipeline</div>
232
  <div class="info-panel">
233
- {#each data.scene.pipeline as step}
234
- <div class="section-label">{step}</div>
235
- {/each}
236
  </div>
237
  </div>
238
  {/if}
239
- {#if data.scene.prompt}
240
  <div class="section">
241
- <div class="section-title">Prompt</div>
242
  <div class="info-panel">
243
- <div class="section-label">{data.scene.prompt}</div>
 
 
244
  </div>
245
  </div>
246
  {/if}
247
- <div class="section">
248
- <div class="section-title">Stats</div>
249
- <div class="info-panel">
250
- <div>FPS: <span bind:this={fpsCount}>0</span></div>
251
- <div>Triangles: <span bind:this={triangleCount}>0</span></div>
252
- </div>
253
- </div>
254
- <div class="section">
255
- <div class="section-title">Render Mode</div>
256
- <div class="button-group mode-list">
257
- <div class="hud-button mode-item active" data-mode="rendered">Rendered</div>
258
- <div class="hud-button mode-item" data-mode="wireframe">Wireframe</div>
259
  </div>
260
- </div>
261
  <div class="section">
262
  <div class="section-title">Actions</div>
263
  <div class="button-group">
@@ -409,10 +330,15 @@
409
  }
410
 
411
  .info-panel {
412
- padding: 10px 10px 0px 10px;
413
  color: #ddd;
414
  }
415
 
 
 
 
 
 
416
  .button-group {
417
  border: none;
418
  background-color: transparent;
 
1
  <script lang="ts">
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: {
9
  title: string;
10
  model: string;
11
+ type: string;
12
  url: string;
13
  prompt: string;
14
  pipeline: string[];
 
18
  };
19
  };
20
 
21
+ let stats: { name: string; value: any }[] = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ let viewer: IViewer;
24
+ let overlay: HTMLDivElement;
25
+ let container: HTMLDivElement;
26
+ let hudToggleBtn: HTMLButtonElement;
27
+ let canvas: HTMLCanvasElement;
28
+ let loadingBarFill: HTMLDivElement;
29
+ let collapsed = false;
 
30
 
31
+ onMount(initViewer);
32
+ onDestroy(destroyViewer);
 
 
 
 
 
 
 
33
 
34
+ async function initViewer() {
35
+ document.body.classList.add("viewer");
36
+ if (data.scene.type == "mesh") {
37
+ viewer = new BabylonViewer(canvas);
38
+ } else if (data.scene.type == "splat") {
39
+ viewer = new SplatViewer(canvas);
40
+ } else {
41
+ console.error(`Unsupported scene type: ${data.scene.type}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
+ handleMobileView();
44
+ await loadModel(data.scene.url);
45
+ window.addEventListener("resize", () => {
46
+ updateCanvasSize();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  });
48
+ updateStats();
49
+ setInterval(updateStats, 1000);
50
+ }
51
 
52
+ function destroyViewer() {
53
+ document.body.classList.remove("viewer");
54
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ function handleMobileView() {
57
+ const isMobile = window.innerWidth < 768;
58
+ if (isMobile) toggleHUD();
59
+ }
60
 
61
+ function toggleHUD() {
62
+ collapsed = !collapsed;
63
+ hudToggleBtn.textContent = collapsed ? ")" : "(";
64
+ if (collapsed) {
65
+ container.classList.remove("hud-expanded");
66
+ } else {
67
+ container.classList.add("hud-expanded");
68
  }
69
+ }
70
 
71
+ function setRenderMode(event: PointerEvent) {
72
+ const babylonViewer = viewer as BabylonViewer;
73
+ if (!babylonViewer) {
74
+ console.error("Can only set render mode for BabylonViewer");
75
+ return;
76
+ }
77
 
78
+ document.querySelectorAll(".mode-item").forEach((item) => {
79
+ item.classList.remove("active");
 
 
 
 
80
  });
 
81
 
82
+ const modeItem = event.currentTarget as HTMLElement;
83
+ modeItem.classList.add("active");
84
 
85
+ const mode = modeItem.dataset.mode as string;
86
+ babylonViewer.setRenderMode(mode);
87
+ }
 
 
 
88
 
89
+ async function loadModel(url: string) {
90
+ overlay.style.display = "flex";
91
+ await viewer.loadModel(url, (progress) => {
92
+ loadingBarFill.style.width = `${progress * 100}%`;
93
  });
 
94
  updateCanvasSize();
95
+ overlay.style.display = "none";
96
  }
97
 
98
  function updateCanvasSize() {
 
101
  canvas.height = container.clientHeight;
102
  }
103
 
104
+ async function capture() {
105
+ const data = await viewer.capture();
106
+ if (!data) {
107
+ console.error("Failed to capture screenshot");
108
+ return;
109
+ }
110
+ const a = document.createElement("a");
111
+ a.href = data;
112
+ a.download = "screenshot.png";
113
+ a.click();
114
+ }
115
+
116
+ function updateStats() {
117
+ stats = viewer.getStats();
118
  }
119
 
120
  function exit() {
 
130
  </div>
131
  <canvas bind:this={canvas} width="512" height="512" />
132
  <div class="exit-button" on:pointerdown={exit}>x</div>
133
+ <div class="hud" class:collapsed>
134
+ <button bind:this={hudToggleBtn} on:click={toggleHUD} class="hud-toggle-btn">(</button>
135
  <div class="section">
136
  <div class="title">{data.scene.title}</div>
137
  </div>
 
140
  <div class="info-panel">
141
  {#if data.scene.model}
142
  <a href={`/models/${data.scene.model}`} class="section-label">{data.model.title}</a>
143
+ {#if data.scene.pipeline}
144
+ <ol class="pipeline">
145
+ {#each data.scene.pipeline as step}
146
+ <li>{step}</li>
147
+ {/each}
148
+ </ol>
149
+ {/if}
150
  {:else}
151
  <div class="section-label">None</div>
152
  {/if}
153
  </div>
154
  </div>
155
+ {#if data.scene.prompt}
156
  <div class="section">
157
+ <div class="section-title">Prompt</div>
158
  <div class="info-panel">
159
+ <div class="section-label">{data.scene.prompt}</div>
 
 
160
  </div>
161
  </div>
162
  {/if}
163
+ {#if stats.length > 0}
164
  <div class="section">
165
+ <div class="section-title">Stats</div>
166
  <div class="info-panel">
167
+ {#each stats as stat}
168
+ <div>{stat.name}: {stat.value}</div>
169
+ {/each}
170
  </div>
171
  </div>
172
  {/if}
173
+ {#if data.scene.type === "mesh"}
174
+ <div class="section">
175
+ <div class="section-title">Render Mode</div>
176
+ <div class="button-group mode-list">
177
+ <div on:pointerdown={setRenderMode} class="hud-button mode-item active" data-mode="rendered">Rendered</div>
178
+ <div on:pointerdown={setRenderMode} class="hud-button mode-item" data-mode="wireframe">Wireframe</div>
179
+ </div>
 
 
 
 
 
180
  </div>
181
+ {/if}
182
  <div class="section">
183
  <div class="section-title">Actions</div>
184
  <div class="button-group">
 
330
  }
331
 
332
  .info-panel {
333
+ padding: 6px 10px 0px 10px;
334
  color: #ddd;
335
  }
336
 
337
+ .pipeline {
338
+ margin: 0;
339
+ padding: 6px 10px 0px 20px;
340
+ }
341
+
342
  .button-group {
343
  border: none;
344
  background-color: transparent;
viewer/src/routes/viewer/[slug]/BabylonViewer.ts ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { IViewer } from "./IViewer";
2
+ import * as BABYLON from '@babylonjs/core';
3
+ import "@babylonjs/loaders/glTF";
4
+ import "@babylonjs/loaders/OBJ";
5
+
6
+ export class BabylonViewer implements IViewer {
7
+ engine: BABYLON.Engine;
8
+ scene: BABYLON.Scene;
9
+ camera: BABYLON.ArcRotateCamera;
10
+ canvas: HTMLCanvasElement;
11
+ triangleCount: number = 0;
12
+
13
+ constructor(canvas: HTMLCanvasElement) {
14
+ this.canvas = canvas;
15
+
16
+ this.engine = new BABYLON.Engine(canvas, true);
17
+
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;
25
+ this.camera.inertia = 0.9;
26
+ this.camera.panningInertia = 0.9;
27
+ this.camera.lowerRadiusLimit = 3;
28
+ this.camera.upperRadiusLimit = 100;
29
+ this.camera.setTarget(BABYLON.Vector3.Zero());
30
+ this.camera.attachControl(this.canvas, true);
31
+ this.camera.onAfterCheckInputsObservable.add(() => {
32
+ this.camera.wheelPrecision = 150 / this.camera.radius;
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) {
42
+ // Load scene
43
+ await BABYLON.SceneLoader.AppendAsync("", url, this.scene, (event) => {
44
+ const progress = event.loaded / event.total;
45
+ loadingBarCallback?.(progress);
46
+ });
47
+
48
+ // Dispose of all cameras and lights
49
+ this.scene.cameras.forEach((camera) => {
50
+ if (camera !== this.camera) {
51
+ camera.dispose();
52
+ }
53
+ });
54
+ this.scene.lights.forEach((light) => {
55
+ light.dispose();
56
+ });
57
+
58
+ // Add lights
59
+ const light = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), this.scene);
60
+ light.intensity = 1;
61
+ light.diffuse = new BABYLON.Color3(1, 1, 1);
62
+ light.groundColor = new BABYLON.Color3(0.3, 0.3, 0.3);
63
+
64
+ const sun = new BABYLON.DirectionalLight("sun", new BABYLON.Vector3(-0.5, -1, -0.5), this.scene);
65
+ sun.intensity = 2;
66
+ sun.diffuse = new BABYLON.Color3(1, 1, 1);
67
+
68
+ // Center and scale model
69
+ const parentNode = new BABYLON.TransformNode("parent", this.scene);
70
+ const standardSize = 10;
71
+ let scaleFactor = 1;
72
+ let center = BABYLON.Vector3.Zero();
73
+ if (this.scene.meshes.length > 0) {
74
+ let bounds = this.scene.meshes[0].getBoundingInfo().boundingBox;
75
+ let min = bounds.minimumWorld;
76
+ let max = bounds.maximumWorld;
77
+
78
+ for (let i = 1; i < this.scene.meshes.length; i++) {
79
+ bounds = this.scene.meshes[i].getBoundingInfo().boundingBox;
80
+ min = BABYLON.Vector3.Minimize(min, bounds.minimumWorld);
81
+ max = BABYLON.Vector3.Maximize(max, bounds.maximumWorld);
82
+ }
83
+
84
+ const extent = max.subtract(min).scale(0.5);
85
+ const size = extent.length();
86
+
87
+ center = BABYLON.Vector3.Center(min, max);
88
+
89
+ scaleFactor = standardSize / size;
90
+ }
91
+ this.triangleCount = 0;
92
+ this.scene.meshes.forEach((mesh) => {
93
+ mesh.setParent(parentNode);
94
+ if (mesh.getTotalVertices() > 0) {
95
+ this.triangleCount += mesh.getTotalIndices() / 3;
96
+ }
97
+ });
98
+ parentNode.position = center.scale(-1 * scaleFactor);
99
+ parentNode.scaling.scaleInPlace(scaleFactor);
100
+
101
+ // Run render loop
102
+ this.engine.runRenderLoop(() => {
103
+ this.scene.render();
104
+ });
105
+ }
106
+
107
+ dispose() {
108
+ if (this.scene) {
109
+ this.scene.dispose();
110
+ }
111
+ }
112
+
113
+ async capture(): Promise<string | null> {
114
+ if (!this.engine || !this.camera) return null;
115
+ const cachedColor = this.scene.clearColor;
116
+ this.scene.clearColor = BABYLON.Color4.FromHexString("#00000000");
117
+ let data = await new Promise<string>((resolve) => {
118
+ BABYLON.Tools.CreateScreenshotUsingRenderTarget(this.engine, this.camera, 512, (result) => {
119
+ resolve(result);
120
+ });
121
+ });
122
+ this.scene.clearColor = cachedColor;
123
+ return data;
124
+ }
125
+
126
+ setRenderMode(mode: string) {
127
+ this.scene.forceWireframe = mode === "wireframe";
128
+ }
129
+
130
+ getStats(): { name: string, value: any }[] {
131
+ const fps = this.engine.getFps().toFixed();
132
+ const triangleCount = this.triangleCount.toLocaleString();
133
+ return [
134
+ { name: "FPS", value: fps },
135
+ { name: "Triangles", value: triangleCount },
136
+ ];
137
+ }
138
+ }
viewer/src/routes/viewer/[slug]/IViewer.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export interface IViewer {
2
+ loadModel(url: string, loadingBarCallback?: (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.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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() {
39
+ return null;
40
+ }
41
+
42
+ getStats(): { name: string, value: any }[] {
43
+ return [];
44
+ }
45
+ }
viewer/static/thumbnails/3dgs-stump.png ADDED