Spaces:
Sleeping
Sleeping
initial release
Browse files- .DS_Store +0 -0
- README.md +59 -6
- app.py +642 -4
- model_utils.py +367 -0
- outputs/.DS_Store +0 -0
- requirements.txt +6 -0
- viewer/.DS_Store +0 -0
- viewer/index.css +367 -0
- viewer/index.html +334 -0
- viewer/index.js +0 -0
- viewer/index.js.map +0 -0
- viewer/settings.default.json +26 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
README.md
CHANGED
|
@@ -1,13 +1,66 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: indigo
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SHARP 3D Gaussian Viewer
|
| 3 |
+
emoji: 🔪
|
| 4 |
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.2.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
models:
|
| 11 |
+
- apple/Sharp
|
| 12 |
+
preload_from_hub:
|
| 13 |
+
- apple/Sharp sharp_2572gikvuh.pt
|
| 14 |
---
|
| 15 |
|
| 16 |
+
# SHARP 3D Gaussian Viewer
|
| 17 |
+
|
| 18 |
+
Interactive 3D Gaussian Splat viewer powered by Apple's SHARP model.
|
| 19 |
+
|
| 20 |
+
## Features
|
| 21 |
+
|
| 22 |
+
- **Single-image 3D reconstruction**: Upload any image to generate a 3D Gaussian splat scene
|
| 23 |
+
- **Interactive 3D viewer**: Explore the scene directly in your browser using SuperSplat Viewer
|
| 24 |
+
- **Focal length control**: Adjust camera FOV for optimal viewing
|
| 25 |
+
- **Download PLY**: Export the generated 3D scene for use in other applications
|
| 26 |
+
- **Open in new tab**: View the 3D scene in a dedicated browser tab
|
| 27 |
+
|
| 28 |
+
## How it Works
|
| 29 |
+
|
| 30 |
+
1. **Upload an image** and optionally adjust the focal length
|
| 31 |
+
2. **SHARP model** (server-side) predicts 3D Gaussians from your image
|
| 32 |
+
3. **SuperSplat Viewer** (client-side) renders the 3D scene in your browser
|
| 33 |
+
|
| 34 |
+
## Controls
|
| 35 |
+
|
| 36 |
+
### Orbit Mode
|
| 37 |
+
- **Left Mouse / One Finger**: Rotate camera
|
| 38 |
+
- **Right Mouse / Two Finger**: Pan camera
|
| 39 |
+
- **Scroll / Pinch**: Zoom in/out
|
| 40 |
+
- **Double Click / Tap**: Set focus point
|
| 41 |
+
|
| 42 |
+
### Fly Mode
|
| 43 |
+
- **Left Mouse**: Look around
|
| 44 |
+
- **W, S, A, D**: Fly movement
|
| 45 |
+
|
| 46 |
+
## Credits
|
| 47 |
+
|
| 48 |
+
- [ml-sharp](https://github.com/apple/ml-sharp) - Apple's SHARP model for single-image 3D Gaussian prediction
|
| 49 |
+
- [supersplat-viewer](https://github.com/playcanvas/supersplat-viewer) - PlayCanvas 3D Gaussian Splat viewer
|
| 50 |
+
|
| 51 |
+
## Citation
|
| 52 |
+
|
| 53 |
+
```bibtex
|
| 54 |
+
@inproceedings{Sharp2025:arxiv,
|
| 55 |
+
title = {Sharp Monocular View Synthesis in Less Than a Second},
|
| 56 |
+
author = {Lars Mescheder and Wei Dong and Shiwei Li and Xuyang Bai and Marcel Santos and Peiyun Hu and Bruno Lecouat and Mingmin Zhen and Amaël Delaunoy and Tian Fang and Yanghai Tsin and Stephan R. Richter and Vladlen Koltun},
|
| 57 |
+
journal = {arXiv preprint arXiv:2512.10685},
|
| 58 |
+
year = {2025},
|
| 59 |
+
}
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## License
|
| 63 |
+
|
| 64 |
+
This demo is provided for research and educational purposes. See the respective licenses for:
|
| 65 |
+
- [SHARP model license](https://github.com/apple/ml-sharp/blob/main/LICENSE)
|
| 66 |
+
- [SuperSplat Viewer (MIT)](https://github.com/playcanvas/supersplat-viewer/blob/main/LICENSE)
|
app.py
CHANGED
|
@@ -1,7 +1,645 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
demo.launch()
|
|
|
|
| 1 |
+
"""SHARP Gradio demo (PLY export + 3D viewer).
|
| 2 |
+
|
| 3 |
+
This Space:
|
| 4 |
+
- Runs Apple's SHARP model to predict a 3D Gaussian scene from a single image.
|
| 5 |
+
- Exports a canonical `.ply` file for download.
|
| 6 |
+
- Serves unique PLY and settings files per generation via the SuperSplat Viewer.
|
| 7 |
+
|
| 8 |
+
Uses Gradio 6's static file serving (no FastAPI/uvicorn needed).
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import math
|
| 15 |
+
import time
|
| 16 |
+
import uuid
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from typing import Final
|
| 19 |
+
|
| 20 |
import gradio as gr
|
| 21 |
|
| 22 |
+
from model_utils import predict_to_ply_gpu
|
| 23 |
+
|
| 24 |
+
# -----------------------------------------------------------------------------
|
| 25 |
+
# Paths & constants
|
| 26 |
+
# -----------------------------------------------------------------------------
|
| 27 |
+
|
| 28 |
+
APP_DIR: Final[Path] = Path(__file__).resolve().parent
|
| 29 |
+
OUTPUTS_DIR: Final[Path] = APP_DIR / "outputs"
|
| 30 |
+
VIEWER_DIR: Final[Path] = APP_DIR / "viewer"
|
| 31 |
+
DEFAULT_SETTINGS_JSON: Final[Path] = VIEWER_DIR / "settings.default.json"
|
| 32 |
+
|
| 33 |
+
DEFAULT_QUEUE_MAX_SIZE: Final[int] = 32
|
| 34 |
+
DEFAULT_FOCAL_LENGTH_MM: Final[float] = 35.0
|
| 35 |
+
SENSOR_HEIGHT_MM: Final[float] = 24.0 # Full-frame 35mm equivalent
|
| 36 |
+
|
| 37 |
+
# Register static paths for Gradio 6 file serving
|
| 38 |
+
gr.set_static_paths(paths=[str(VIEWER_DIR), str(OUTPUTS_DIR)])
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
THEME: Final = gr.themes.Origin()
|
| 42 |
+
|
| 43 |
+
CSS: Final[str] = """
|
| 44 |
+
/* Keep layout stable when scrollbars appear/disappear */
|
| 45 |
+
html { scrollbar-gutter: stable; }
|
| 46 |
+
|
| 47 |
+
/* Use normal document flow */
|
| 48 |
+
html, body { height: auto; }
|
| 49 |
+
body { overflow: auto; }
|
| 50 |
+
|
| 51 |
+
/* Full-width layout */
|
| 52 |
+
.gradio-container {
|
| 53 |
+
max-width: none;
|
| 54 |
+
width: 100%;
|
| 55 |
+
margin: 0;
|
| 56 |
+
padding: 0.5rem 1rem 1rem;
|
| 57 |
+
box-sizing: border-box;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* Header styling */
|
| 61 |
+
#app-header {
|
| 62 |
+
margin-bottom: 0.5rem;
|
| 63 |
+
}
|
| 64 |
+
#app-header h2 {
|
| 65 |
+
margin: 0 0 0.25rem 0;
|
| 66 |
+
font-size: 1.5rem;
|
| 67 |
+
}
|
| 68 |
+
#app-header p {
|
| 69 |
+
margin: 0;
|
| 70 |
+
opacity: 0.85;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* Main layout: controls left, viewer right (larger) */
|
| 74 |
+
#main-row {
|
| 75 |
+
gap: 1rem;
|
| 76 |
+
align-items: stretch;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* Left panel: controls */
|
| 80 |
+
#controls-panel {
|
| 81 |
+
display: flex;
|
| 82 |
+
flex-direction: column;
|
| 83 |
+
gap: 0.75rem;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
#controls-panel .input-image-container {
|
| 87 |
+
flex: 1;
|
| 88 |
+
min-height: 200px;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
#input-image {
|
| 92 |
+
width: 100%;
|
| 93 |
+
}
|
| 94 |
+
#input-image img {
|
| 95 |
+
width: 100%;
|
| 96 |
+
height: auto;
|
| 97 |
+
max-height: 280px;
|
| 98 |
+
object-fit: contain;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* Options row */
|
| 102 |
+
#options-row {
|
| 103 |
+
gap: 0.5rem;
|
| 104 |
+
}
|
| 105 |
+
#options-row > div {
|
| 106 |
+
flex: 1;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* Action buttons */
|
| 110 |
+
#actions-row {
|
| 111 |
+
gap: 0.5rem;
|
| 112 |
+
}
|
| 113 |
+
#actions-row button {
|
| 114 |
+
flex: 1;
|
| 115 |
+
min-height: 42px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Downloads row */
|
| 119 |
+
#downloads-row {
|
| 120 |
+
gap: 0.5rem;
|
| 121 |
+
align-items: center;
|
| 122 |
+
}
|
| 123 |
+
#downloads-row > div {
|
| 124 |
+
flex: 1;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* Right panel: 3D viewer (dominant) */
|
| 128 |
+
#viewer-panel {
|
| 129 |
+
display: flex;
|
| 130 |
+
flex-direction: column;
|
| 131 |
+
min-height: 500px;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
#viewer-container {
|
| 135 |
+
flex: 1;
|
| 136 |
+
display: flex;
|
| 137 |
+
flex-direction: column;
|
| 138 |
+
min-height: 0;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* Viewer iframe/placeholder */
|
| 142 |
+
#viewer-html {
|
| 143 |
+
flex: 1;
|
| 144 |
+
min-height: 500px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
#viewer-html iframe {
|
| 148 |
+
width: 100%;
|
| 149 |
+
height: 100%;
|
| 150 |
+
min-height: 500px;
|
| 151 |
+
border: 0;
|
| 152 |
+
border-radius: 12px;
|
| 153 |
+
overflow: hidden;
|
| 154 |
+
background: #000;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/* Placeholder styling */
|
| 158 |
+
.viewer-placeholder {
|
| 159 |
+
width: 100%;
|
| 160 |
+
height: 100%;
|
| 161 |
+
min-height: 500px;
|
| 162 |
+
display: flex;
|
| 163 |
+
align-items: center;
|
| 164 |
+
justify-content: center;
|
| 165 |
+
border: 2px dashed var(--border-color-primary, rgba(127, 127, 127, 0.35));
|
| 166 |
+
border-radius: 12px;
|
| 167 |
+
background: var(--block-background-fill, rgba(127, 127, 127, 0.05));
|
| 168 |
+
color: var(--body-text-color, rgba(255, 255, 255, 0.92));
|
| 169 |
+
transition: all 0.3s ease;
|
| 170 |
+
}
|
| 171 |
+
.viewer-placeholder-inner {
|
| 172 |
+
max-width: 400px;
|
| 173 |
+
padding: 32px;
|
| 174 |
+
text-align: center;
|
| 175 |
+
}
|
| 176 |
+
.viewer-placeholder-icon {
|
| 177 |
+
font-size: 48px;
|
| 178 |
+
margin-bottom: 16px;
|
| 179 |
+
opacity: 0.6;
|
| 180 |
+
}
|
| 181 |
+
.viewer-placeholder-title {
|
| 182 |
+
font-size: 18px;
|
| 183 |
+
font-weight: 600;
|
| 184 |
+
margin-bottom: 8px;
|
| 185 |
+
}
|
| 186 |
+
.viewer-placeholder-desc {
|
| 187 |
+
font-size: 14px;
|
| 188 |
+
line-height: 1.5;
|
| 189 |
+
opacity: 0.75;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/* Loading state */
|
| 193 |
+
.viewer-loading {
|
| 194 |
+
border-color: var(--primary-500;
|
| 195 |
+
background: linear-gradient(
|
| 196 |
+
135deg,
|
| 197 |
+
rgba(255, 102, 0, 0.05) 0%,
|
| 198 |
+
rgba(255, 102, 0, 0.1) 100%
|
| 199 |
+
);
|
| 200 |
+
}
|
| 201 |
+
.viewer-loading .viewer-placeholder-icon {
|
| 202 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 203 |
+
}
|
| 204 |
+
@keyframes pulse {
|
| 205 |
+
0%, 100% { opacity: 0.4; transform: scale(1); }
|
| 206 |
+
50% { opacity: 0.8; transform: scale(1.05); }
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/* Status text */
|
| 210 |
+
#status-text {
|
| 211 |
+
font-size: 13px;
|
| 212 |
+
opacity: 0.85;
|
| 213 |
+
margin-top: 0.5rem;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/* Responsive: stack on small screens */
|
| 217 |
+
@media (max-width: 900px) {
|
| 218 |
+
#main-row {
|
| 219 |
+
flex-direction: column;
|
| 220 |
+
}
|
| 221 |
+
#controls-panel, #viewer-panel {
|
| 222 |
+
min-width: 100% !important;
|
| 223 |
+
}
|
| 224 |
+
#viewer-html, #viewer-html iframe, .viewer-placeholder {
|
| 225 |
+
min-height: 400px;
|
| 226 |
+
}
|
| 227 |
+
#input-image img {
|
| 228 |
+
max-height: 200px;
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
"""
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _ensure_dir(path: Path) -> Path:
|
| 235 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 236 |
+
return path
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
_ensure_dir(OUTPUTS_DIR)
|
| 240 |
+
_ensure_dir(VIEWER_DIR)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# -----------------------------------------------------------------------------
|
| 244 |
+
# FOV / Focal Length utilities
|
| 245 |
+
# -----------------------------------------------------------------------------
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def focal_length_to_fov(focal_length_mm: float, sensor_height_mm: float = SENSOR_HEIGHT_MM) -> float:
|
| 249 |
+
"""Convert focal length (mm) to vertical field of view (degrees).
|
| 250 |
+
|
| 251 |
+
Uses the formula: FOV = 2 * atan(sensor_height / (2 * focal_length))
|
| 252 |
+
"""
|
| 253 |
+
if focal_length_mm <= 0:
|
| 254 |
+
focal_length_mm = DEFAULT_FOCAL_LENGTH_MM
|
| 255 |
+
fov_rad = 2 * math.atan(sensor_height_mm / (2 * focal_length_mm))
|
| 256 |
+
return math.degrees(fov_rad)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def create_settings_file(focal_length_mm: float, output_stem: str) -> Path:
|
| 260 |
+
"""Create a unique settings.json for this generation."""
|
| 261 |
+
fov = focal_length_to_fov(focal_length_mm)
|
| 262 |
+
|
| 263 |
+
# Load default settings as base
|
| 264 |
+
settings = {
|
| 265 |
+
"camera": {
|
| 266 |
+
"fov": fov,
|
| 267 |
+
"position": [0, 1, -1],
|
| 268 |
+
"target": [0, 0, 0],
|
| 269 |
+
"startAnim": "none",
|
| 270 |
+
"animTrack": ""
|
| 271 |
+
},
|
| 272 |
+
"background": {"color": [0, 0, 0, 0]},
|
| 273 |
+
"animTracks": []
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
if DEFAULT_SETTINGS_JSON.exists():
|
| 277 |
+
try:
|
| 278 |
+
existing = json.loads(DEFAULT_SETTINGS_JSON.read_text(encoding="utf-8"))
|
| 279 |
+
# Merge, preserving existing values but updating FOV
|
| 280 |
+
if "background" in existing:
|
| 281 |
+
settings["background"] = existing["background"]
|
| 282 |
+
if "camera" in existing:
|
| 283 |
+
settings["camera"] = {**settings["camera"], **existing["camera"]}
|
| 284 |
+
settings["camera"]["fov"] = fov # Always update FOV
|
| 285 |
+
if "animTracks" in existing:
|
| 286 |
+
settings["animTracks"] = existing["animTracks"]
|
| 287 |
+
except Exception:
|
| 288 |
+
pass
|
| 289 |
+
|
| 290 |
+
settings_path = OUTPUTS_DIR / f"{output_stem}.settings.json"
|
| 291 |
+
settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
|
| 292 |
+
return settings_path
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
# -----------------------------------------------------------------------------
|
| 296 |
+
# Validation & file operations
|
| 297 |
+
# -----------------------------------------------------------------------------
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _validate_image(image_path: str | None) -> None:
|
| 301 |
+
if not image_path:
|
| 302 |
+
raise gr.Error("Please upload an image first.")
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _generate_output_stem() -> str:
|
| 306 |
+
"""Generate unique output file stem."""
|
| 307 |
+
ts = int(time.time() * 1000)
|
| 308 |
+
uid = uuid.uuid4().hex[:8]
|
| 309 |
+
return f"scene_{ts}_{uid}"
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
# -----------------------------------------------------------------------------
|
| 313 |
+
# HTML generators
|
| 314 |
+
# -----------------------------------------------------------------------------
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def viewer_url_for_output(ply_filename: str, settings_filename: str) -> str:
|
| 318 |
+
"""URL for the viewer with specific output files."""
|
| 319 |
+
# Use absolute paths with /gradio_api/file= prefix for content and settings
|
| 320 |
+
content_path = f"/gradio_api/file=outputs/{ply_filename}"
|
| 321 |
+
settings_path = f"/gradio_api/file=outputs/{settings_filename}"
|
| 322 |
+
return f"/gradio_api/file=viewer/index.html?content={content_path}&settings={settings_path}&noanim"
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def viewer_placeholder_html() -> str:
|
| 326 |
+
return """
|
| 327 |
+
<div class="viewer-placeholder">
|
| 328 |
+
<div class="viewer-placeholder-inner">
|
| 329 |
+
<div class="viewer-placeholder-icon">🎨</div>
|
| 330 |
+
<div class="viewer-placeholder-title">3D Viewer</div>
|
| 331 |
+
<div class="viewer-placeholder-desc">
|
| 332 |
+
Upload an image and click <strong>Generate</strong> to create a 3D Gaussian scene.
|
| 333 |
+
The interactive viewer will appear here.
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
"""
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def viewer_loading_html() -> str:
|
| 341 |
+
"""Loading placeholder with timer element."""
|
| 342 |
+
return """
|
| 343 |
+
<div class="viewer-placeholder viewer-loading">
|
| 344 |
+
<div class="viewer-placeholder-inner">
|
| 345 |
+
<div class="viewer-placeholder-icon">⚡</div>
|
| 346 |
+
<div class="viewer-placeholder-title">Generating 3D Scene...</div>
|
| 347 |
+
<div class="viewer-placeholder-desc">
|
| 348 |
+
Running SHARP model inference. This may take a moment.
|
| 349 |
+
</div>
|
| 350 |
+
<div id="generation-timer" style="font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 28px; font-weight: 600; color: var(--primary-500); margin-top: 16px;">0s</div>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
"""
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def viewer_iframe_html(ply_filename: str, settings_filename: str) -> str:
|
| 357 |
+
src = viewer_url_for_output(ply_filename, settings_filename)
|
| 358 |
+
return f"""
|
| 359 |
+
<iframe
|
| 360 |
+
src="{src}"
|
| 361 |
+
allow="xr-spatial-tracking; fullscreen"
|
| 362 |
+
referrerpolicy="no-referrer"
|
| 363 |
+
loading="eager"
|
| 364 |
+
></iframe>
|
| 365 |
+
"""
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
# -----------------------------------------------------------------------------
|
| 369 |
+
# Main inference function
|
| 370 |
+
# -----------------------------------------------------------------------------
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def run_sharp(
|
| 374 |
+
image_path: str | None,
|
| 375 |
+
focal_length_mm: float,
|
| 376 |
+
) -> tuple[object, object, str, object, object, object, str]:
|
| 377 |
+
"""Run SHARP inference.
|
| 378 |
+
|
| 379 |
+
Returns: (ply_download, viewer_html, status, generate_btn, clear_btn, open_viewer_btn, current_viewer_url)
|
| 380 |
+
"""
|
| 381 |
+
_validate_image(image_path)
|
| 382 |
+
|
| 383 |
+
try:
|
| 384 |
+
# Generate unique output stem
|
| 385 |
+
output_stem = _generate_output_stem()
|
| 386 |
+
|
| 387 |
+
# Create settings file with FOV
|
| 388 |
+
settings_path = create_settings_file(focal_length_mm, output_stem)
|
| 389 |
+
|
| 390 |
+
# Run inference
|
| 391 |
+
ply_path = predict_to_ply_gpu(image_path)
|
| 392 |
+
|
| 393 |
+
# Rename PLY to unique name in outputs
|
| 394 |
+
unique_ply_path = OUTPUTS_DIR / f"{output_stem}.ply"
|
| 395 |
+
ply_path.rename(unique_ply_path)
|
| 396 |
+
|
| 397 |
+
fov = focal_length_to_fov(focal_length_mm)
|
| 398 |
+
status = f"✓ Generated **{unique_ply_path.name}** | FOV: {fov:.1f}°"
|
| 399 |
+
|
| 400 |
+
viewer_url = viewer_url_for_output(unique_ply_path.name, settings_path.name)
|
| 401 |
+
|
| 402 |
+
return (
|
| 403 |
+
gr.update(value=str(unique_ply_path), visible=True, interactive=True),
|
| 404 |
+
viewer_iframe_html(unique_ply_path.name, settings_path.name),
|
| 405 |
+
status,
|
| 406 |
+
gr.update(interactive=True, value="Generate"),
|
| 407 |
+
gr.update(interactive=True),
|
| 408 |
+
gr.update(visible=True, interactive=True),
|
| 409 |
+
viewer_url,
|
| 410 |
+
)
|
| 411 |
+
except gr.Error:
|
| 412 |
+
raise
|
| 413 |
+
except Exception as e:
|
| 414 |
+
raise gr.Error(f"Generation failed: {type(e).__name__}: {e}") from e
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def start_generation() -> tuple[str, object, object]:
|
| 418 |
+
"""Start generation: show loading state.
|
| 419 |
+
|
| 420 |
+
Returns: (viewer_html, generate_btn, clear_btn)
|
| 421 |
+
"""
|
| 422 |
+
return (
|
| 423 |
+
viewer_loading_html(),
|
| 424 |
+
gr.update(interactive=False, value="Generating..."),
|
| 425 |
+
gr.update(interactive=False),
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def clear_all() -> tuple:
|
| 430 |
+
"""Clear all inputs and outputs.
|
| 431 |
+
|
| 432 |
+
Returns: (image, ply_download, viewer_html, status, generate_btn, clear_btn, open_viewer_btn, current_viewer_url)
|
| 433 |
+
"""
|
| 434 |
+
return (
|
| 435 |
+
None,
|
| 436 |
+
gr.update(value=None, visible=False),
|
| 437 |
+
viewer_placeholder_html(),
|
| 438 |
+
"",
|
| 439 |
+
gr.update(interactive=True, value="Generate"),
|
| 440 |
+
gr.update(interactive=False),
|
| 441 |
+
gr.update(visible=False),
|
| 442 |
+
"",
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def on_image_change(image_path: str | None) -> tuple[object, object]:
|
| 447 |
+
"""Handle image upload/removal.
|
| 448 |
+
|
| 449 |
+
Returns: (generate_btn, clear_btn)
|
| 450 |
+
"""
|
| 451 |
+
has_image = bool(image_path)
|
| 452 |
+
return (
|
| 453 |
+
gr.update(interactive=has_image, value="Generate"),
|
| 454 |
+
gr.update(interactive=has_image),
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
# -----------------------------------------------------------------------------
|
| 459 |
+
# UI
|
| 460 |
+
# -----------------------------------------------------------------------------
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
# Global JS for timer control and viewer URL (injected via head parameter)
|
| 464 |
+
HEAD_JS: Final[str] = """
|
| 465 |
+
<script>
|
| 466 |
+
window.sharpTimer = {
|
| 467 |
+
interval: null,
|
| 468 |
+
start: function() {
|
| 469 |
+
this.stop();
|
| 470 |
+
var startTime = Date.now();
|
| 471 |
+
this.interval = setInterval(function() {
|
| 472 |
+
var el = document.getElementById('generation-timer');
|
| 473 |
+
if (!el) return;
|
| 474 |
+
var secs = Math.floor((Date.now() - startTime) / 1000);
|
| 475 |
+
var mins = Math.floor(secs / 60);
|
| 476 |
+
secs = secs % 60;
|
| 477 |
+
el.textContent = mins > 0 ? mins + ':' + (secs < 10 ? '0' : '') + secs : secs + 's';
|
| 478 |
+
}, 500);
|
| 479 |
+
},
|
| 480 |
+
stop: function() {
|
| 481 |
+
if (this.interval) {
|
| 482 |
+
clearInterval(this.interval);
|
| 483 |
+
this.interval = null;
|
| 484 |
+
}
|
| 485 |
+
}
|
| 486 |
+
};
|
| 487 |
+
window.openSharpViewer = function() {
|
| 488 |
+
var iframe = document.querySelector('#viewer-html iframe');
|
| 489 |
+
if (iframe && iframe.src) {
|
| 490 |
+
window.open(iframe.src, '_blank');
|
| 491 |
+
}
|
| 492 |
+
};
|
| 493 |
+
</script>
|
| 494 |
+
"""
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
def build_demo() -> gr.Blocks:
|
| 498 |
+
with gr.Blocks(
|
| 499 |
+
title="SHARP • Single-Image 3D Gaussian Prediction",
|
| 500 |
+
elem_id="sharp-root",
|
| 501 |
+
) as demo:
|
| 502 |
+
# Hidden textbox to store viewer URL (State doesn't work well with js param)
|
| 503 |
+
current_viewer_url = gr.Textbox(value="", visible=False, elem_id="viewer-url-store")
|
| 504 |
+
|
| 505 |
+
# Header
|
| 506 |
+
with gr.Column(elem_id="app-header"):
|
| 507 |
+
gr.Markdown("## SHARP")
|
| 508 |
+
gr.Markdown("Single-image **3D Gaussian scene** prediction")
|
| 509 |
+
|
| 510 |
+
# Main layout: controls (left, narrow) + viewer (right, wide)
|
| 511 |
+
with gr.Row(elem_id="main-row", equal_height=True):
|
| 512 |
+
# Left column: Controls
|
| 513 |
+
with gr.Column(scale=3, min_width=280, elem_id="controls-panel"):
|
| 514 |
+
# Image upload
|
| 515 |
+
image_in = gr.Image(
|
| 516 |
+
label="Input Image",
|
| 517 |
+
type="filepath",
|
| 518 |
+
sources=["upload"],
|
| 519 |
+
elem_id="input-image",
|
| 520 |
+
show_label=True,
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
# Options
|
| 524 |
+
with gr.Row(elem_id="options-row"):
|
| 525 |
+
focal_length = gr.Slider(
|
| 526 |
+
label="Focal Length (mm)",
|
| 527 |
+
minimum=12,
|
| 528 |
+
maximum=200,
|
| 529 |
+
step=1,
|
| 530 |
+
value=DEFAULT_FOCAL_LENGTH_MM,
|
| 531 |
+
info="Affects viewer FOV",
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
+
# Action buttons
|
| 535 |
+
with gr.Row(elem_id="actions-row"):
|
| 536 |
+
generate_btn = gr.Button(
|
| 537 |
+
"Generate",
|
| 538 |
+
variant="primary",
|
| 539 |
+
interactive=False,
|
| 540 |
+
elem_id="generate-btn",
|
| 541 |
+
)
|
| 542 |
+
clear_btn = gr.Button(
|
| 543 |
+
"Clear",
|
| 544 |
+
variant="secondary",
|
| 545 |
+
interactive=False,
|
| 546 |
+
elem_id="clear-btn",
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
# Downloads
|
| 550 |
+
with gr.Row(elem_id="downloads-row"):
|
| 551 |
+
ply_download = gr.DownloadButton(
|
| 552 |
+
label="Download PLY",
|
| 553 |
+
value=None,
|
| 554 |
+
visible=False,
|
| 555 |
+
elem_id="ply-download",
|
| 556 |
+
)
|
| 557 |
+
open_viewer_btn = gr.Button(
|
| 558 |
+
"Open Viewer in New Tab ↗",
|
| 559 |
+
size="sm",
|
| 560 |
+
visible=False,
|
| 561 |
+
elem_id="open-viewer-btn",
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
# Status
|
| 565 |
+
status_md = gr.Markdown("", elem_id="status-text")
|
| 566 |
+
|
| 567 |
+
# Right column: 3D Viewer (dominant)
|
| 568 |
+
with gr.Column(scale=7, min_width=400, elem_id="viewer-panel"):
|
| 569 |
+
viewer_html = gr.HTML(
|
| 570 |
+
value=viewer_placeholder_html(),
|
| 571 |
+
elem_id="viewer-html",
|
| 572 |
+
label="3D Viewer",
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
# About section (collapsible)
|
| 576 |
+
with gr.Accordion("About", open=False):
|
| 577 |
+
gr.Markdown("""
|
| 578 |
+
### SHARP Model
|
| 579 |
+
**Sharp Monocular View Synthesis in Less Than a Second** (Apple, 2025)
|
| 580 |
+
|
| 581 |
+
SHARP predicts a 3D Gaussian splatting scene from a single image, enabling novel view synthesis.
|
| 582 |
+
|
| 583 |
+
```bibtex
|
| 584 |
+
@inproceedings{Sharp2025:arxiv,
|
| 585 |
+
title = {Sharp Monocular View Synthesis in Less Than a Second},
|
| 586 |
+
author = {Lars Mescheder and Wei Dong and Shiwei Li and Xuyang Bai and Marcel Santos and Peiyun Hu and Bruno Lecouat and Mingmin Zhen and Amaël Delaunoy and Tian Fang and Yanghai Tsin and Stephan R. Richter and Vladlen Koltun},
|
| 587 |
+
journal = {arXiv preprint arXiv:2512.10685},
|
| 588 |
+
year = {2025},
|
| 589 |
+
}
|
| 590 |
+
```
|
| 591 |
+
|
| 592 |
+
### 3D Viewer
|
| 593 |
+
Powered by [SuperSplat Viewer](https://github.com/playcanvas/supersplat-viewer) by PlayCanvas.
|
| 594 |
+
""".strip())
|
| 595 |
+
|
| 596 |
+
# --- Event handlers ---
|
| 597 |
+
|
| 598 |
+
# Image change: enable/disable buttons
|
| 599 |
+
image_in.change(
|
| 600 |
+
fn=on_image_change,
|
| 601 |
+
inputs=[image_in],
|
| 602 |
+
outputs=[generate_btn, clear_btn],
|
| 603 |
+
queue=False,
|
| 604 |
+
show_progress="hidden",
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
# Generate: start loading, run inference
|
| 608 |
+
generate_btn.click(
|
| 609 |
+
fn=start_generation,
|
| 610 |
+
outputs=[viewer_html, generate_btn, clear_btn],
|
| 611 |
+
queue=False,
|
| 612 |
+
show_progress="hidden",
|
| 613 |
+
js="() => { window.sharpTimer && window.sharpTimer.start(); }",
|
| 614 |
+
).then(
|
| 615 |
+
fn=run_sharp,
|
| 616 |
+
inputs=[image_in, focal_length],
|
| 617 |
+
outputs=[ply_download, viewer_html, status_md, generate_btn, clear_btn, open_viewer_btn, current_viewer_url],
|
| 618 |
+
show_progress="hidden",
|
| 619 |
+
).then(
|
| 620 |
+
fn=lambda: None,
|
| 621 |
+
js="() => { window.sharpTimer && window.sharpTimer.stop(); }",
|
| 622 |
+
)
|
| 623 |
+
|
| 624 |
+
# Clear
|
| 625 |
+
clear_btn.click(
|
| 626 |
+
fn=clear_all,
|
| 627 |
+
outputs=[image_in, ply_download, viewer_html, status_md, generate_btn, clear_btn, open_viewer_btn, current_viewer_url],
|
| 628 |
+
queue=False,
|
| 629 |
+
show_progress="hidden",
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
# Open viewer in new tab using global URL
|
| 633 |
+
open_viewer_btn.click(
|
| 634 |
+
fn=None,
|
| 635 |
+
js="() => { window.openSharpViewer(); }",
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
demo.queue(max_size=DEFAULT_QUEUE_MAX_SIZE, default_concurrency_limit=1)
|
| 639 |
+
return demo
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
demo = build_demo()
|
| 643 |
|
| 644 |
+
if __name__ == "__main__":
|
| 645 |
+
demo.launch(theme=THEME, css=CSS, head=HEAD_JS)
|
model_utils.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SHARP inference utilities (PLY export only).
|
| 2 |
+
|
| 3 |
+
This module intentionally does *not* implement MP4/video rendering.
|
| 4 |
+
It provides a small, Spaces/ZeroGPU-friendly wrapper that:
|
| 5 |
+
- Caches model weights and predictor construction across requests.
|
| 6 |
+
- Runs SHARP inference and exports a canonical `.ply`.
|
| 7 |
+
|
| 8 |
+
Public API (used by the Gradio app):
|
| 9 |
+
- predict_to_ply_gpu(...)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import threading
|
| 16 |
+
import time
|
| 17 |
+
import uuid
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Final
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
import spaces
|
| 26 |
+
except Exception: # pragma: no cover
|
| 27 |
+
spaces = None # type: ignore[assignment]
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
# Prefer HF cache / Hub downloads (works with Spaces `preload_from_hub`).
|
| 31 |
+
from huggingface_hub import hf_hub_download, try_to_load_from_cache
|
| 32 |
+
except Exception: # pragma: no cover
|
| 33 |
+
hf_hub_download = None # type: ignore[assignment]
|
| 34 |
+
try_to_load_from_cache = None # type: ignore[assignment]
|
| 35 |
+
|
| 36 |
+
from sharp.cli.predict import DEFAULT_MODEL_URL, predict_image
|
| 37 |
+
from sharp.models import PredictorParams, create_predictor
|
| 38 |
+
from sharp.utils import io
|
| 39 |
+
from sharp.utils.gaussians import save_ply
|
| 40 |
+
|
| 41 |
+
# -----------------------------------------------------------------------------
|
| 42 |
+
# Helpers
|
| 43 |
+
# -----------------------------------------------------------------------------
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _now_ms() -> int:
|
| 47 |
+
return int(time.time() * 1000)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _ensure_dir(path: Path) -> Path:
|
| 51 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 52 |
+
return path
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _make_even(x: int) -> int:
|
| 56 |
+
return x if x % 2 == 0 else x + 1
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _select_device(preference: str = "auto") -> torch.device:
|
| 60 |
+
"""Select the best available device for inference (CPU/CUDA/MPS)."""
|
| 61 |
+
if preference not in {"auto", "cpu", "cuda", "mps"}:
|
| 62 |
+
raise ValueError("device preference must be one of: auto|cpu|cuda|mps")
|
| 63 |
+
|
| 64 |
+
if preference == "cpu":
|
| 65 |
+
return torch.device("cpu")
|
| 66 |
+
if preference == "cuda":
|
| 67 |
+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 68 |
+
if preference == "mps":
|
| 69 |
+
return torch.device("mps" if torch.backends.mps.is_available() else "cpu")
|
| 70 |
+
|
| 71 |
+
# auto
|
| 72 |
+
if torch.cuda.is_available():
|
| 73 |
+
return torch.device("cuda")
|
| 74 |
+
if torch.backends.mps.is_available():
|
| 75 |
+
return torch.device("mps")
|
| 76 |
+
return torch.device("cpu")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# -----------------------------------------------------------------------------
|
| 80 |
+
# Prediction outputs
|
| 81 |
+
# -----------------------------------------------------------------------------
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@dataclass(frozen=True, slots=True)
|
| 85 |
+
class PredictionOutputs:
|
| 86 |
+
"""Outputs of SHARP inference."""
|
| 87 |
+
|
| 88 |
+
ply_path: Path
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
# -----------------------------------------------------------------------------
|
| 92 |
+
# Model wrapper
|
| 93 |
+
# -----------------------------------------------------------------------------
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class ModelWrapper:
|
| 97 |
+
"""Cached SHARP model wrapper for Gradio/Spaces."""
|
| 98 |
+
|
| 99 |
+
def __init__(
|
| 100 |
+
self,
|
| 101 |
+
*,
|
| 102 |
+
outputs_dir: str | Path = "outputs",
|
| 103 |
+
checkpoint_url: str = DEFAULT_MODEL_URL,
|
| 104 |
+
checkpoint_path: str | Path | None = None,
|
| 105 |
+
device_preference: str = "auto",
|
| 106 |
+
keep_model_on_device: bool | None = None,
|
| 107 |
+
hf_repo_id: str | None = None,
|
| 108 |
+
hf_filename: str | None = None,
|
| 109 |
+
hf_revision: str | None = None,
|
| 110 |
+
) -> None:
|
| 111 |
+
self.outputs_dir = _ensure_dir(Path(outputs_dir))
|
| 112 |
+
self.checkpoint_url = checkpoint_url
|
| 113 |
+
|
| 114 |
+
env_ckpt = os.getenv("SHARP_CHECKPOINT_PATH") or os.getenv("SHARP_CHECKPOINT")
|
| 115 |
+
if checkpoint_path:
|
| 116 |
+
self.checkpoint_path = Path(checkpoint_path)
|
| 117 |
+
elif env_ckpt:
|
| 118 |
+
self.checkpoint_path = Path(env_ckpt)
|
| 119 |
+
else:
|
| 120 |
+
self.checkpoint_path = None
|
| 121 |
+
|
| 122 |
+
# Optional Hugging Face Hub fallback (useful when direct CDN download fails).
|
| 123 |
+
self.hf_repo_id = hf_repo_id or os.getenv("SHARP_HF_REPO_ID", "apple/Sharp")
|
| 124 |
+
self.hf_filename = hf_filename or os.getenv(
|
| 125 |
+
"SHARP_HF_FILENAME", "sharp_2572gikvuh.pt"
|
| 126 |
+
)
|
| 127 |
+
self.hf_revision = hf_revision or os.getenv("SHARP_HF_REVISION") or None
|
| 128 |
+
|
| 129 |
+
self.device_preference = device_preference
|
| 130 |
+
|
| 131 |
+
# For ZeroGPU, it's safer to not keep large tensors on CUDA across calls.
|
| 132 |
+
if keep_model_on_device is None:
|
| 133 |
+
keep_env = (
|
| 134 |
+
os.getenv("SHARP_KEEP_MODEL_ON_DEVICE")
|
| 135 |
+
)
|
| 136 |
+
self.keep_model_on_device = keep_env == "1"
|
| 137 |
+
else:
|
| 138 |
+
self.keep_model_on_device = keep_model_on_device
|
| 139 |
+
|
| 140 |
+
self._lock = threading.RLock()
|
| 141 |
+
self._predictor: torch.nn.Module | None = None
|
| 142 |
+
self._predictor_device: torch.device | None = None
|
| 143 |
+
self._state_dict: dict | None = None
|
| 144 |
+
|
| 145 |
+
def has_cuda(self) -> bool:
|
| 146 |
+
return torch.cuda.is_available()
|
| 147 |
+
|
| 148 |
+
def _load_state_dict(self) -> dict:
|
| 149 |
+
with self._lock:
|
| 150 |
+
if self._state_dict is not None:
|
| 151 |
+
return self._state_dict
|
| 152 |
+
|
| 153 |
+
# 1) Explicit local checkpoint path
|
| 154 |
+
if self.checkpoint_path is not None:
|
| 155 |
+
try:
|
| 156 |
+
self._state_dict = torch.load(
|
| 157 |
+
self.checkpoint_path,
|
| 158 |
+
weights_only=True,
|
| 159 |
+
map_location="cpu",
|
| 160 |
+
)
|
| 161 |
+
return self._state_dict
|
| 162 |
+
except Exception as e:
|
| 163 |
+
raise RuntimeError(
|
| 164 |
+
"Failed to load SHARP checkpoint from local path.\n\n"
|
| 165 |
+
f"Path:\n {self.checkpoint_path}\n\n"
|
| 166 |
+
f"Original error:\n {type(e).__name__}: {e}"
|
| 167 |
+
) from e
|
| 168 |
+
|
| 169 |
+
# 2) HF cache (no-network): best match for Spaces `preload_from_hub`.
|
| 170 |
+
hf_cache_error: Exception | None = None
|
| 171 |
+
if try_to_load_from_cache is not None:
|
| 172 |
+
try:
|
| 173 |
+
cached = try_to_load_from_cache(
|
| 174 |
+
repo_id=self.hf_repo_id,
|
| 175 |
+
filename=self.hf_filename,
|
| 176 |
+
revision=self.hf_revision,
|
| 177 |
+
repo_type="model",
|
| 178 |
+
)
|
| 179 |
+
except TypeError:
|
| 180 |
+
cached = try_to_load_from_cache(self.hf_repo_id, self.hf_filename) # type: ignore[misc]
|
| 181 |
+
|
| 182 |
+
try:
|
| 183 |
+
if isinstance(cached, str) and Path(cached).exists():
|
| 184 |
+
self._state_dict = torch.load(
|
| 185 |
+
cached, weights_only=True, map_location="cpu"
|
| 186 |
+
)
|
| 187 |
+
return self._state_dict
|
| 188 |
+
except Exception as e:
|
| 189 |
+
hf_cache_error = e
|
| 190 |
+
|
| 191 |
+
# 3) HF Hub download (reuse cache when available; may download otherwise).
|
| 192 |
+
hf_error: Exception | None = None
|
| 193 |
+
if hf_hub_download is not None:
|
| 194 |
+
# Attempt "local only" mode if supported (avoids network).
|
| 195 |
+
try:
|
| 196 |
+
import inspect
|
| 197 |
+
|
| 198 |
+
if "local_files_only" in inspect.signature(hf_hub_download).parameters:
|
| 199 |
+
ckpt_path = hf_hub_download(
|
| 200 |
+
repo_id=self.hf_repo_id,
|
| 201 |
+
filename=self.hf_filename,
|
| 202 |
+
revision=self.hf_revision,
|
| 203 |
+
local_files_only=True,
|
| 204 |
+
)
|
| 205 |
+
if Path(ckpt_path).exists():
|
| 206 |
+
self._state_dict = torch.load(
|
| 207 |
+
ckpt_path, weights_only=True, map_location="cpu"
|
| 208 |
+
)
|
| 209 |
+
return self._state_dict
|
| 210 |
+
except Exception:
|
| 211 |
+
pass
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
ckpt_path = hf_hub_download(
|
| 215 |
+
repo_id=self.hf_repo_id,
|
| 216 |
+
filename=self.hf_filename,
|
| 217 |
+
revision=self.hf_revision,
|
| 218 |
+
)
|
| 219 |
+
self._state_dict = torch.load(
|
| 220 |
+
ckpt_path,
|
| 221 |
+
weights_only=True,
|
| 222 |
+
map_location="cpu",
|
| 223 |
+
)
|
| 224 |
+
return self._state_dict
|
| 225 |
+
except Exception as e:
|
| 226 |
+
hf_error = e
|
| 227 |
+
|
| 228 |
+
# 4) Default upstream CDN (torch hub cache). Last resort.
|
| 229 |
+
url_error: Exception | None = None
|
| 230 |
+
try:
|
| 231 |
+
self._state_dict = torch.hub.load_state_dict_from_url(
|
| 232 |
+
self.checkpoint_url,
|
| 233 |
+
progress=True,
|
| 234 |
+
map_location="cpu",
|
| 235 |
+
)
|
| 236 |
+
return self._state_dict
|
| 237 |
+
except Exception as e:
|
| 238 |
+
url_error = e
|
| 239 |
+
|
| 240 |
+
# If we got here: all options failed.
|
| 241 |
+
hint_lines = [
|
| 242 |
+
"Failed to load SHARP checkpoint.",
|
| 243 |
+
"",
|
| 244 |
+
"Tried (in order):",
|
| 245 |
+
f" 1) HF cache (preload_from_hub): repo_id={self.hf_repo_id}, filename={self.hf_filename}, revision={self.hf_revision or 'None'}",
|
| 246 |
+
f" 2) HF Hub download: repo_id={self.hf_repo_id}, filename={self.hf_filename}, revision={self.hf_revision or 'None'}",
|
| 247 |
+
f" 3) URL (torch hub): {self.checkpoint_url}",
|
| 248 |
+
"",
|
| 249 |
+
"If network access is restricted, set a local checkpoint path:",
|
| 250 |
+
" - SHARP_CHECKPOINT_PATH=/path/to/sharp_2572gikvuh.pt",
|
| 251 |
+
"",
|
| 252 |
+
"Original errors:",
|
| 253 |
+
]
|
| 254 |
+
if try_to_load_from_cache is None:
|
| 255 |
+
hint_lines.append(" HF cache: huggingface_hub not installed")
|
| 256 |
+
elif hf_cache_error is not None:
|
| 257 |
+
hint_lines.append(
|
| 258 |
+
f" HF cache: {type(hf_cache_error).__name__}: {hf_cache_error}"
|
| 259 |
+
)
|
| 260 |
+
else:
|
| 261 |
+
hint_lines.append(" HF cache: (not found in cache)")
|
| 262 |
+
|
| 263 |
+
if hf_hub_download is None:
|
| 264 |
+
hint_lines.append(" HF download: huggingface_hub not installed")
|
| 265 |
+
else:
|
| 266 |
+
hint_lines.append(f" HF download: {type(hf_error).__name__}: {hf_error}")
|
| 267 |
+
|
| 268 |
+
hint_lines.append(f" URL: {type(url_error).__name__}: {url_error}")
|
| 269 |
+
|
| 270 |
+
raise RuntimeError("\n".join(hint_lines))
|
| 271 |
+
|
| 272 |
+
def _get_predictor(self, device: torch.device) -> torch.nn.Module:
|
| 273 |
+
with self._lock:
|
| 274 |
+
if self._predictor is None:
|
| 275 |
+
state_dict = self._load_state_dict()
|
| 276 |
+
predictor = create_predictor(PredictorParams())
|
| 277 |
+
predictor.load_state_dict(state_dict)
|
| 278 |
+
predictor.eval()
|
| 279 |
+
self._predictor = predictor
|
| 280 |
+
self._predictor_device = torch.device("cpu")
|
| 281 |
+
|
| 282 |
+
assert self._predictor is not None
|
| 283 |
+
assert self._predictor_device is not None
|
| 284 |
+
|
| 285 |
+
if self._predictor_device != device:
|
| 286 |
+
self._predictor.to(device)
|
| 287 |
+
self._predictor_device = device
|
| 288 |
+
|
| 289 |
+
return self._predictor
|
| 290 |
+
|
| 291 |
+
def _maybe_move_model_back_to_cpu(self) -> None:
|
| 292 |
+
if self.keep_model_on_device:
|
| 293 |
+
return
|
| 294 |
+
with self._lock:
|
| 295 |
+
if self._predictor is not None and self._predictor_device is not None:
|
| 296 |
+
if self._predictor_device.type != "cpu":
|
| 297 |
+
self._predictor.to("cpu")
|
| 298 |
+
self._predictor_device = torch.device("cpu")
|
| 299 |
+
if torch.cuda.is_available():
|
| 300 |
+
torch.cuda.empty_cache()
|
| 301 |
+
|
| 302 |
+
def _make_output_stem(self, input_path: Path) -> str:
|
| 303 |
+
return f"{input_path.stem}-{_now_ms()}-{uuid.uuid4().hex[:8]}"
|
| 304 |
+
|
| 305 |
+
def predict_to_ply(self, image_path: str | Path) -> PredictionOutputs:
|
| 306 |
+
"""Run SHARP inference and export a .ply file."""
|
| 307 |
+
image_path = Path(image_path)
|
| 308 |
+
if not image_path.exists():
|
| 309 |
+
raise FileNotFoundError(f"Image does not exist: {image_path}")
|
| 310 |
+
|
| 311 |
+
device = _select_device(self.device_preference)
|
| 312 |
+
predictor = self._get_predictor(device)
|
| 313 |
+
|
| 314 |
+
image_np, _, f_px = io.load_rgb(image_path)
|
| 315 |
+
height, width = image_np.shape[:2]
|
| 316 |
+
|
| 317 |
+
with torch.no_grad():
|
| 318 |
+
gaussians = predict_image(predictor, image_np, f_px, device)
|
| 319 |
+
|
| 320 |
+
stem = self._make_output_stem(image_path)
|
| 321 |
+
ply_path = self.outputs_dir / f"{stem}.ply"
|
| 322 |
+
|
| 323 |
+
# save_ply expects (height, width).
|
| 324 |
+
save_ply(gaussians, f_px, (height, width), ply_path)
|
| 325 |
+
|
| 326 |
+
self._maybe_move_model_back_to_cpu()
|
| 327 |
+
|
| 328 |
+
return PredictionOutputs(ply_path=ply_path)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
# -----------------------------------------------------------------------------
|
| 332 |
+
# ZeroGPU entrypoints
|
| 333 |
+
# -----------------------------------------------------------------------------
|
| 334 |
+
#
|
| 335 |
+
# IMPORTANT: Do NOT decorate bound instance methods with `@spaces.GPU` on ZeroGPU.
|
| 336 |
+
# The wrapper uses multiprocessing queues and pickles args/kwargs. If `self` is
|
| 337 |
+
# included, Python will try to pickle the whole instance. ModelWrapper contains
|
| 338 |
+
# a threading.RLock (not pickleable) and the model itself should not be pickled.
|
| 339 |
+
#
|
| 340 |
+
# Expose module-level functions that accept only pickleable arguments and
|
| 341 |
+
# create/cache the ModelWrapper inside the GPU worker process.
|
| 342 |
+
|
| 343 |
+
DEFAULT_OUTPUTS_DIR: Final[Path] = _ensure_dir(Path(__file__).resolve().parent / "outputs")
|
| 344 |
+
|
| 345 |
+
_GLOBAL_MODEL: ModelWrapper | None = None
|
| 346 |
+
_GLOBAL_MODEL_INIT_LOCK: Final[threading.Lock] = threading.Lock()
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def get_global_model(*, outputs_dir: str | Path = DEFAULT_OUTPUTS_DIR) -> ModelWrapper:
|
| 350 |
+
global _GLOBAL_MODEL
|
| 351 |
+
with _GLOBAL_MODEL_INIT_LOCK:
|
| 352 |
+
if _GLOBAL_MODEL is None:
|
| 353 |
+
_GLOBAL_MODEL = ModelWrapper(outputs_dir=outputs_dir)
|
| 354 |
+
return _GLOBAL_MODEL
|
| 355 |
+
|
| 356 |
+
def predict_to_ply(
|
| 357 |
+
image_path: str | Path,
|
| 358 |
+
) -> Path:
|
| 359 |
+
model = get_global_model()
|
| 360 |
+
return model.predict_to_ply(image_path).ply_path
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
# Export the GPU-wrapped callable (or a no-op wrapper locally).
|
| 364 |
+
if spaces is not None:
|
| 365 |
+
predict_to_ply_gpu = spaces.GPU(duration=180)(predict_to_ply)
|
| 366 |
+
else: # pragma: no cover
|
| 367 |
+
predict_to_ply_gpu = predict_to_ply
|
outputs/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.2.0
|
| 2 |
+
spaces==0.44.0
|
| 3 |
+
huggingface_hub>=1.2.3
|
| 4 |
+
torch
|
| 5 |
+
torchvision
|
| 6 |
+
sharp @ git+https://github.com/apple/ml-sharp.git@cdb4ddc6796402bee5487c7312260f2edd8bd5f0
|
viewer/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
viewer/index.css
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--canvas-opacity: 1;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
* {
|
| 6 |
+
margin: 0;
|
| 7 |
+
padding: 0;
|
| 8 |
+
touch-action: none;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
body {
|
| 12 |
+
overflow: hidden;
|
| 13 |
+
font-family: "Arial", sans-serif;
|
| 14 |
+
-moz-user-select: none;
|
| 15 |
+
user-select: none;
|
| 16 |
+
-webkit-user-select: none;
|
| 17 |
+
-webkit-touch-callout: none;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
canvas {
|
| 21 |
+
opacity: var(--canvas-opacity);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
button {
|
| 25 |
+
-webkit-appearance: none;
|
| 26 |
+
-moz-appearance: none;
|
| 27 |
+
appearance: none;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.hidden {
|
| 31 |
+
display: none !important;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.spacer {
|
| 35 |
+
flex-grow: 1;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
#ui {
|
| 39 |
+
position: fixed;
|
| 40 |
+
top: 0;
|
| 41 |
+
left: 0;
|
| 42 |
+
width: 100%;
|
| 43 |
+
height: 100%;
|
| 44 |
+
pointer-events: none;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
#ui * {
|
| 48 |
+
pointer-events: auto;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* poster */
|
| 52 |
+
#poster {
|
| 53 |
+
display: none;
|
| 54 |
+
position: absolute;
|
| 55 |
+
top: 0;
|
| 56 |
+
left: 0;
|
| 57 |
+
width: 100%;
|
| 58 |
+
height: 100%;
|
| 59 |
+
background-image: var(--poster-url);
|
| 60 |
+
background-size: cover;
|
| 61 |
+
background-position: center;
|
| 62 |
+
background-repeat: no-repeat;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* loadingWrap */
|
| 66 |
+
#loadingWrap {
|
| 67 |
+
position: fixed;
|
| 68 |
+
bottom: 120px;
|
| 69 |
+
left: 50%;
|
| 70 |
+
transform: translate(-50%, 0);
|
| 71 |
+
width: 380px;
|
| 72 |
+
display: flex;
|
| 73 |
+
flex-direction: column;
|
| 74 |
+
padding: 16px;
|
| 75 |
+
}
|
| 76 |
+
#loadingWrap > #loadingText {
|
| 77 |
+
font-size: 18px;
|
| 78 |
+
color: #fff;
|
| 79 |
+
text-align: center;
|
| 80 |
+
text-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
|
| 81 |
+
}
|
| 82 |
+
#loadingWrap > #loadingBar {
|
| 83 |
+
width: 100%;
|
| 84 |
+
height: 10px;
|
| 85 |
+
margin-top: 8px;
|
| 86 |
+
border-radius: 4px;
|
| 87 |
+
overflow: hidden;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/* controlsWrap */
|
| 91 |
+
#controlsWrap {
|
| 92 |
+
position: absolute;
|
| 93 |
+
left: max(16px, env(safe-area-inset-left));
|
| 94 |
+
right: max(16px, env(safe-area-inset-right));
|
| 95 |
+
bottom: max(16px, env(safe-area-inset-bottom));
|
| 96 |
+
display: flex;
|
| 97 |
+
flex-direction: column;
|
| 98 |
+
}
|
| 99 |
+
#controlsWrap.faded-in {
|
| 100 |
+
visibility: visible;
|
| 101 |
+
opacity: 1;
|
| 102 |
+
transition: opacity 0.5s ease-out;
|
| 103 |
+
}
|
| 104 |
+
#controlsWrap.faded-out {
|
| 105 |
+
visibility: hidden;
|
| 106 |
+
opacity: 0;
|
| 107 |
+
transition: visibility 0s 0.5s, opacity 0.5s ease-out;
|
| 108 |
+
}
|
| 109 |
+
#controlsWrap > #timelineContainer {
|
| 110 |
+
height: 30px;
|
| 111 |
+
cursor: pointer;
|
| 112 |
+
}
|
| 113 |
+
#controlsWrap > #timelineContainer > #line {
|
| 114 |
+
width: 100%;
|
| 115 |
+
height: 50%;
|
| 116 |
+
border-bottom: 4px solid #d9d9d9;
|
| 117 |
+
}
|
| 118 |
+
#controlsWrap > #timelineContainer > #handle {
|
| 119 |
+
position: absolute;
|
| 120 |
+
top: 16.5px;
|
| 121 |
+
width: 12px;
|
| 122 |
+
height: 12px;
|
| 123 |
+
transform: translate(-50%, -50%);
|
| 124 |
+
border: 2px solid #d9d9d9;
|
| 125 |
+
border-radius: 50%;
|
| 126 |
+
background-color: #FFAF50;
|
| 127 |
+
}
|
| 128 |
+
#controlsWrap > #timelineContainer > #time {
|
| 129 |
+
position: absolute;
|
| 130 |
+
top: 0;
|
| 131 |
+
padding: 2px 4px;
|
| 132 |
+
transform: translate(-50%, -100%);
|
| 133 |
+
font-size: 12px;
|
| 134 |
+
border-radius: 4px;
|
| 135 |
+
color: #fff;
|
| 136 |
+
background-color: rgba(40, 40, 40, 0.5);
|
| 137 |
+
}
|
| 138 |
+
#controlsWrap > #buttonsContainer {
|
| 139 |
+
display: flex;
|
| 140 |
+
gap: 8px;
|
| 141 |
+
/* controlButton */
|
| 142 |
+
}
|
| 143 |
+
#controlsWrap > #buttonsContainer .controlButton {
|
| 144 |
+
width: 40px;
|
| 145 |
+
height: 40px;
|
| 146 |
+
padding: 0;
|
| 147 |
+
margin: 0;
|
| 148 |
+
border: 0;
|
| 149 |
+
cursor: pointer;
|
| 150 |
+
color: #E0DCDD;
|
| 151 |
+
background-color: transparent;
|
| 152 |
+
}
|
| 153 |
+
#controlsWrap > #buttonsContainer .controlButton:hover {
|
| 154 |
+
color: #fff;
|
| 155 |
+
}
|
| 156 |
+
#controlsWrap > #buttonsContainer .controlButton {
|
| 157 |
+
/* icon styling */
|
| 158 |
+
}
|
| 159 |
+
#controlsWrap > #buttonsContainer .controlButton > svg {
|
| 160 |
+
display: block;
|
| 161 |
+
margin: auto;
|
| 162 |
+
}
|
| 163 |
+
#controlsWrap > #buttonsContainer .controlButton > svg > g.stroke {
|
| 164 |
+
fill: none;
|
| 165 |
+
stroke: black;
|
| 166 |
+
stroke-width: 2;
|
| 167 |
+
stroke-linejoin: round;
|
| 168 |
+
opacity: 0.4;
|
| 169 |
+
}
|
| 170 |
+
#controlsWrap > #buttonsContainer .controlButton > svg > g.fill {
|
| 171 |
+
fill: currentColor;
|
| 172 |
+
stroke: none;
|
| 173 |
+
}
|
| 174 |
+
#controlsWrap > #buttonsContainer .controlButton {
|
| 175 |
+
/* camera toggle styling */
|
| 176 |
+
}
|
| 177 |
+
#controlsWrap > #buttonsContainer .controlButton.toggle {
|
| 178 |
+
background: linear-gradient(90deg, transparent 0%, transparent 50%, #F60 50%, #F60 100%);
|
| 179 |
+
background-size: 200% 100%;
|
| 180 |
+
background-position: 100% 0%;
|
| 181 |
+
background-repeat: no-repeat;
|
| 182 |
+
transition: background-position 0.1s ease-in-out;
|
| 183 |
+
}
|
| 184 |
+
#controlsWrap > #buttonsContainer .controlButton.toggle.left {
|
| 185 |
+
margin-right: -4px;
|
| 186 |
+
border-radius: 4px 0px 0px 4px;
|
| 187 |
+
}
|
| 188 |
+
#controlsWrap > #buttonsContainer .controlButton.toggle.left:not(.active) {
|
| 189 |
+
background-position: 0% 0%;
|
| 190 |
+
}
|
| 191 |
+
#controlsWrap > #buttonsContainer .controlButton.toggle.right {
|
| 192 |
+
margin-left: -4px;
|
| 193 |
+
margin-right: 20px;
|
| 194 |
+
border-radius: 0px 4px 4px 0px;
|
| 195 |
+
}
|
| 196 |
+
#controlsWrap > #buttonsContainer .controlButton.toggle.right:not(.active) {
|
| 197 |
+
background-position: 200% 0%;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* settingsPanel */
|
| 201 |
+
#settingsPanel {
|
| 202 |
+
position: fixed;
|
| 203 |
+
right: max(16px, env(safe-area-inset-right));
|
| 204 |
+
bottom: calc(max(16px, env(safe-area-inset-bottom)) + 70px);
|
| 205 |
+
padding: 10px;
|
| 206 |
+
border-radius: 8px;
|
| 207 |
+
display: flex;
|
| 208 |
+
flex-direction: column;
|
| 209 |
+
align-items: flex-end;
|
| 210 |
+
gap: 4px;
|
| 211 |
+
font-size: 14px;
|
| 212 |
+
color: #E0DCDD;
|
| 213 |
+
background-color: #333;
|
| 214 |
+
}
|
| 215 |
+
#settingsPanel > .settingsRow {
|
| 216 |
+
display: flex;
|
| 217 |
+
gap: 4px;
|
| 218 |
+
width: 100%;
|
| 219 |
+
}
|
| 220 |
+
#settingsPanel > .settingsRow > button {
|
| 221 |
+
flex-grow: 1;
|
| 222 |
+
padding: 10px 20px;
|
| 223 |
+
border: 0;
|
| 224 |
+
cursor: pointer;
|
| 225 |
+
color: #E0DCDD;
|
| 226 |
+
background-color: #141414;
|
| 227 |
+
}
|
| 228 |
+
#settingsPanel > .settingsRow > button:hover {
|
| 229 |
+
color: #fff;
|
| 230 |
+
}
|
| 231 |
+
#settingsPanel > .settingsRow > div {
|
| 232 |
+
padding: 8px;
|
| 233 |
+
cursor: pointer;
|
| 234 |
+
color: #AAA;
|
| 235 |
+
}
|
| 236 |
+
#settingsPanel > .settingsRow > div.checkMark {
|
| 237 |
+
width: 16px;
|
| 238 |
+
}
|
| 239 |
+
#settingsPanel > .settingsRow > div.checkMark:not(.active) {
|
| 240 |
+
color: #333;
|
| 241 |
+
}
|
| 242 |
+
#settingsPanel > .settingsRow > div:hover {
|
| 243 |
+
color: #E0DCDD;
|
| 244 |
+
}
|
| 245 |
+
#settingsPanel > .divider {
|
| 246 |
+
width: 100%;
|
| 247 |
+
height: 1px;
|
| 248 |
+
margin: 8px 0;
|
| 249 |
+
background-color: #666;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/* infoPanel */
|
| 253 |
+
#infoPanel {
|
| 254 |
+
position: fixed;
|
| 255 |
+
top: 0;
|
| 256 |
+
left: 0;
|
| 257 |
+
width: 100%;
|
| 258 |
+
height: 100%;
|
| 259 |
+
background-color: rgba(0, 0, 0, 0.4);
|
| 260 |
+
backdrop-filter: blur(2px);
|
| 261 |
+
}
|
| 262 |
+
#infoPanel > #infoPanelContent {
|
| 263 |
+
position: absolute;
|
| 264 |
+
top: 40px;
|
| 265 |
+
left: 50%;
|
| 266 |
+
transform: translate(-50%, 0);
|
| 267 |
+
min-height: 280px;
|
| 268 |
+
min-width: 320px;
|
| 269 |
+
padding: 8px;
|
| 270 |
+
border-radius: 24px;
|
| 271 |
+
display: flex;
|
| 272 |
+
flex-direction: column;
|
| 273 |
+
color: #E0DCDD;
|
| 274 |
+
background-color: rgba(51, 51, 51, 0.8);
|
| 275 |
+
}
|
| 276 |
+
#infoPanel > #infoPanelContent > #tabs {
|
| 277 |
+
display: flex;
|
| 278 |
+
gap: 16px;
|
| 279 |
+
padding: 8px;
|
| 280 |
+
border-radius: 22px;
|
| 281 |
+
background-color: #282828;
|
| 282 |
+
}
|
| 283 |
+
#infoPanel > #infoPanelContent > #tabs > .tab {
|
| 284 |
+
padding: 8px;
|
| 285 |
+
border-radius: 16px;
|
| 286 |
+
cursor: pointer;
|
| 287 |
+
flex-grow: 1;
|
| 288 |
+
text-align: center;
|
| 289 |
+
font-weight: bold;
|
| 290 |
+
font-size: 14px;
|
| 291 |
+
color: #E0DCDD;
|
| 292 |
+
transition: background-color 250ms ease;
|
| 293 |
+
}
|
| 294 |
+
#infoPanel > #infoPanelContent > #tabs > .tab:hover {
|
| 295 |
+
background-color: #444;
|
| 296 |
+
}
|
| 297 |
+
#infoPanel > #infoPanelContent > #tabs > .tab.active {
|
| 298 |
+
background-color: #444;
|
| 299 |
+
}
|
| 300 |
+
#infoPanel > #infoPanelContent > #infoPanels {
|
| 301 |
+
padding: 16px;
|
| 302 |
+
}
|
| 303 |
+
#infoPanel > #infoPanelContent > #infoPanels h1 {
|
| 304 |
+
font-size: 14px;
|
| 305 |
+
font-weight: bold;
|
| 306 |
+
padding: 0 0 6px 0;
|
| 307 |
+
color: #fff;
|
| 308 |
+
}
|
| 309 |
+
#infoPanel > #infoPanelContent > #infoPanels .control-item {
|
| 310 |
+
display: flex;
|
| 311 |
+
justify-content: space-between;
|
| 312 |
+
gap: 32px;
|
| 313 |
+
line-height: 1.5;
|
| 314 |
+
}
|
| 315 |
+
#infoPanel > #infoPanelContent > #infoPanels .control-item > .control-action {
|
| 316 |
+
text-align: left;
|
| 317 |
+
}
|
| 318 |
+
#infoPanel > #infoPanelContent > #infoPanels .control-item > .control-key {
|
| 319 |
+
text-align: right;
|
| 320 |
+
}
|
| 321 |
+
#infoPanel > #infoPanelContent > #infoPanels .control-spacer {
|
| 322 |
+
border-bottom: 1px dashed #666;
|
| 323 |
+
margin: 10px 0;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
#joystickBase {
|
| 327 |
+
position: absolute;
|
| 328 |
+
width: 96px;
|
| 329 |
+
height: 96px;
|
| 330 |
+
transform: translate(-50%, -50%);
|
| 331 |
+
border-radius: 50%;
|
| 332 |
+
touch-action: none;
|
| 333 |
+
background: radial-gradient(circle at center, rgba(0, 0, 0, 0) 50%, black 100%);
|
| 334 |
+
background-color: rgba(0, 0, 0, 0.1333333333);
|
| 335 |
+
}
|
| 336 |
+
#joystickBase > #joystick {
|
| 337 |
+
position: absolute;
|
| 338 |
+
width: 48px;
|
| 339 |
+
height: 48px;
|
| 340 |
+
transform: translate(-50%, -50%);
|
| 341 |
+
border-radius: 50%;
|
| 342 |
+
touch-action: none;
|
| 343 |
+
background-color: rgba(255, 255, 255, 0.5333333333);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
#tooltip {
|
| 347 |
+
display: none;
|
| 348 |
+
position: absolute;
|
| 349 |
+
border-radius: 4px;
|
| 350 |
+
padding: 4px 4px;
|
| 351 |
+
font-size: 12px;
|
| 352 |
+
color: #E0DCDD;
|
| 353 |
+
background-color: #282828;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
#annotations {
|
| 357 |
+
position: fixed;
|
| 358 |
+
top: 0;
|
| 359 |
+
left: 0;
|
| 360 |
+
width: 100%;
|
| 361 |
+
height: 100%;
|
| 362 |
+
pointer-events: none;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
#annotations * {
|
| 366 |
+
pointer-events: auto;
|
| 367 |
+
}
|
viewer/index.html
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<title>SuperSplat Viewer</title>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 7 |
+
<base href="">
|
| 8 |
+
<link rel="stylesheet" href="./index.css">
|
| 9 |
+
<script type="module">
|
| 10 |
+
const createImage = (url) => {
|
| 11 |
+
const img = new Image();
|
| 12 |
+
img.src = url;
|
| 13 |
+
return img;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const url = new URL(location.href);
|
| 17 |
+
|
| 18 |
+
const posterUrl = url.searchParams.get('poster');
|
| 19 |
+
const skyboxUrl = url.searchParams.get('skybox');
|
| 20 |
+
const settingsUrl = url.searchParams.has('settings') ? url.searchParams.get('settings') : './settings.json';
|
| 21 |
+
const contentUrl = url.searchParams.has('content') ? url.searchParams.get('content') : './scene.ply';
|
| 22 |
+
|
| 23 |
+
const sseConfig = {
|
| 24 |
+
poster: posterUrl && createImage(posterUrl),
|
| 25 |
+
skyboxUrl,
|
| 26 |
+
contentUrl,
|
| 27 |
+
contents: fetch(contentUrl),
|
| 28 |
+
noui: url.searchParams.has('noui'),
|
| 29 |
+
noanim: url.searchParams.has('noanim'),
|
| 30 |
+
ministats: url.searchParams.has('ministats'),
|
| 31 |
+
colorize: url.searchParams.has('colorize'),
|
| 32 |
+
unified: url.searchParams.has('unified'),
|
| 33 |
+
aa: url.searchParams.has('aa')
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
window.sse = {
|
| 37 |
+
config: sseConfig,
|
| 38 |
+
settings: fetch(settingsUrl).then(response => response.json())
|
| 39 |
+
};
|
| 40 |
+
</script>
|
| 41 |
+
</head>
|
| 42 |
+
<body>
|
| 43 |
+
<pc-app antialias="false" depth="true" high-resolution="true" stencil="false">
|
| 44 |
+
<pc-scene>
|
| 45 |
+
<!-- Camera (with XR support) -->
|
| 46 |
+
<pc-entity name="camera root">
|
| 47 |
+
<pc-entity name="camera"></pc-entity>
|
| 48 |
+
</pc-entity>
|
| 49 |
+
<!-- Light (for XR controllers) -->
|
| 50 |
+
<pc-entity name="light" rotation="35 45 0">
|
| 51 |
+
<pc-light color="white" intensity="1.5"></pc-light>
|
| 52 |
+
</pc-entity>
|
| 53 |
+
<!-- Splat -->
|
| 54 |
+
<pc-entity name="splat" rotation="0 0 180">
|
| 55 |
+
</pc-entity>
|
| 56 |
+
</pc-scene>
|
| 57 |
+
</pc-app>
|
| 58 |
+
|
| 59 |
+
<div id="ui">
|
| 60 |
+
<div id="poster"></div>
|
| 61 |
+
|
| 62 |
+
<!-- Loading Indicator -->
|
| 63 |
+
<div id="loadingWrap">
|
| 64 |
+
<div id="loadingText"></div>
|
| 65 |
+
<div id="loadingBar"></div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div id="controlsWrap" class="faded-in">
|
| 69 |
+
|
| 70 |
+
<!-- Timeline Panel -->
|
| 71 |
+
<div id="timelineContainer" class="hidden">
|
| 72 |
+
<div id="line"></div>
|
| 73 |
+
<div id="handle"></div>
|
| 74 |
+
<div id="time" class="hidden">0:00</div>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<!-- Buttons Panel -->
|
| 78 |
+
<div id="buttonsContainer">
|
| 79 |
+
<button id="play" class="controlButton hidden">
|
| 80 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
| 81 |
+
<g class='stroke'><use href="#playIcon"/></g>
|
| 82 |
+
<g class='fill'><use href="#playIcon"/></g>
|
| 83 |
+
</svg>
|
| 84 |
+
</button>
|
| 85 |
+
<button id="pause" class="controlButton hidden">
|
| 86 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px">
|
| 87 |
+
<g class='stroke'><use href="#pauseIcon"/></g>
|
| 88 |
+
<g class='fill'><use href="#pauseIcon"/></g>
|
| 89 |
+
</svg>
|
| 90 |
+
</button>
|
| 91 |
+
|
| 92 |
+
<div class="spacer"></div>
|
| 93 |
+
|
| 94 |
+
<button id="orbitCamera" class="controlButton toggle left">
|
| 95 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
| 96 |
+
<g class='stroke'><use href="#orbitIcon"/></g>
|
| 97 |
+
<g class='fill'><use href="#orbitIcon"/></g>
|
| 98 |
+
</svg>
|
| 99 |
+
</button>
|
| 100 |
+
<button id="flyCamera" class="controlButton toggle right">
|
| 101 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
| 102 |
+
<g class='stroke'><use href="#flyIcon"/></g>
|
| 103 |
+
<g class='fill'><use href="#flyIcon"/></g>
|
| 104 |
+
</svg>
|
| 105 |
+
</button>
|
| 106 |
+
|
| 107 |
+
<button id="arMode" class="controlButton hidden">
|
| 108 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28">
|
| 109 |
+
<g class='stroke'><use href="#arIcon"/></g>
|
| 110 |
+
<g class='fill'><use href="#arIcon"/></g>
|
| 111 |
+
</svg>
|
| 112 |
+
</button>
|
| 113 |
+
<button id="vrMode" class="controlButton hidden">
|
| 114 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28">
|
| 115 |
+
<g class='stroke'><use href="#vrIcon"/></g>
|
| 116 |
+
<g class='fill'><use href="#vrIcon"/></g>
|
| 117 |
+
</svg>
|
| 118 |
+
</button>
|
| 119 |
+
|
| 120 |
+
<button id="info" class="controlButton">
|
| 121 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
| 122 |
+
<g class='stroke'><use href="#infoIcon"/></g>
|
| 123 |
+
<g class='fill'><use href="#infoIcon"/></g>
|
| 124 |
+
</svg>
|
| 125 |
+
</button>
|
| 126 |
+
<button id="settings" class="controlButton">
|
| 127 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
| 128 |
+
<g class='stroke'><use href="#settingsIcon"/></g>
|
| 129 |
+
<g class='fill'><use href="#settingsIcon"/></g>
|
| 130 |
+
</svg>
|
| 131 |
+
</button>
|
| 132 |
+
|
| 133 |
+
<button id="enterFullscreen" class="controlButton">
|
| 134 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
| 135 |
+
<g class='stroke'><use href="#enterFullscreenIcon"/></g>
|
| 136 |
+
<g class='fill'><use href="#enterFullscreenIcon"/></g>
|
| 137 |
+
</svg>
|
| 138 |
+
</button>
|
| 139 |
+
<button id="exitFullscreen" class="controlButton hidden">
|
| 140 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
| 141 |
+
<g class='stroke'><use href="#exitFullscreenIcon"/></g>
|
| 142 |
+
<g class='fill'><use href="#exitFullscreenIcon"/></g>
|
| 143 |
+
</svg>
|
| 144 |
+
</button>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<!-- Settings Panel -->
|
| 149 |
+
<div id="settingsPanel" class="hidden">
|
| 150 |
+
<div class="settingsRow">
|
| 151 |
+
<div id="hqCheck" class="checkMark">✓</div>
|
| 152 |
+
<div id="hqOption">High Quality Render</div>
|
| 153 |
+
</div>
|
| 154 |
+
<div class="settingsRow">
|
| 155 |
+
<div id="lqCheck" class="checkMark">✓</div>
|
| 156 |
+
<div id="lqOption">Low Quality Render</div>
|
| 157 |
+
</div>
|
| 158 |
+
<div class="divider"></div>
|
| 159 |
+
<div class="settingsRow">
|
| 160 |
+
<button id="frame">Frame</button>
|
| 161 |
+
<button id="reset">Reset</button>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
+
<!-- Info Panel -->
|
| 166 |
+
<div id="infoPanel" class="hidden">
|
| 167 |
+
<div id="infoPanelContent" onpointerdown="event.stopPropagation()">
|
| 168 |
+
<div id="tabs">
|
| 169 |
+
<div id="desktopTab" class="tab active">Desktop</div>
|
| 170 |
+
<div id="touchTab" class="tab">Touch</div>
|
| 171 |
+
</div>
|
| 172 |
+
<div id="infoPanels">
|
| 173 |
+
<div id="desktopInfoPanel">
|
| 174 |
+
<div class="control-spacer"></div>
|
| 175 |
+
<h1>Orbit Mode</h1>
|
| 176 |
+
<div class="control-item">
|
| 177 |
+
<span class="control-action">Orbit</span>
|
| 178 |
+
<span class="control-key">Left Mouse</span>
|
| 179 |
+
</div>
|
| 180 |
+
<div class="control-item">
|
| 181 |
+
<span class="control-action">Pan</span>
|
| 182 |
+
<span class="control-key">Right Mouse</span>
|
| 183 |
+
</div>
|
| 184 |
+
<div class="control-item">
|
| 185 |
+
<span class="control-action">Zoom</span>
|
| 186 |
+
<span class="control-key">Mouse Wheel</span>
|
| 187 |
+
</div>
|
| 188 |
+
<div class="control-item">
|
| 189 |
+
<span class="control-action">Set Focus</span>
|
| 190 |
+
<span class="control-key">Double Click</span>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="control-spacer"></div>
|
| 193 |
+
<h1>Fly Mode</h1>
|
| 194 |
+
<div class="control-item">
|
| 195 |
+
<span class="control-action">Look Around</span>
|
| 196 |
+
<span class="control-key">Left Mouse</span>
|
| 197 |
+
</div>
|
| 198 |
+
<div class="control-item">
|
| 199 |
+
<span class="control-action">Fly</span>
|
| 200 |
+
<span class="control-key">W,S,A,D</span>
|
| 201 |
+
</div>
|
| 202 |
+
<div class="control-spacer"></div>
|
| 203 |
+
<div class="control-item">
|
| 204 |
+
<span class="control-action">Frame Scene</span>
|
| 205 |
+
<span class="control-key">F</span>
|
| 206 |
+
</div>
|
| 207 |
+
<div class="control-item">
|
| 208 |
+
<span class="control-action">Reset Camera</span>
|
| 209 |
+
<span class="control-key">R</span>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
<div id="touchInfoPanel" class="hidden">
|
| 213 |
+
<div class="control-spacer"></div>
|
| 214 |
+
<h1>Orbit Mode</h1>
|
| 215 |
+
<div class="control-item">
|
| 216 |
+
<span class="control-action">Orbit</span>
|
| 217 |
+
<span class="control-key">One Finger Drag</span>
|
| 218 |
+
</div>
|
| 219 |
+
<div class="control-item">
|
| 220 |
+
<span class="control-action">Pan</span>
|
| 221 |
+
<span class="control-key">Two Finger Drag</span>
|
| 222 |
+
</div>
|
| 223 |
+
<div class="control-item">
|
| 224 |
+
<span class="control-action">Zoom</span>
|
| 225 |
+
<span class="control-key">Pinch</span>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="control-item">
|
| 228 |
+
<span class="control-action">Set Focus</span>
|
| 229 |
+
<span class="control-key">Double Tap</span>
|
| 230 |
+
</div>
|
| 231 |
+
<div class="control-spacer"></div>
|
| 232 |
+
<h1>Fly Mode</h1>
|
| 233 |
+
<div class="control-item">
|
| 234 |
+
<span class="control-action">Look Around</span>
|
| 235 |
+
<span class="control-key">Touch on Right</span>
|
| 236 |
+
</div>
|
| 237 |
+
<div class="control-item">
|
| 238 |
+
<span class="control-action">Fly</span>
|
| 239 |
+
<span class="control-key">Touch on Left</span>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<!-- Touch Joystick -->
|
| 247 |
+
<div id="joystickBase" class="hidden">
|
| 248 |
+
<div id="joystick"></div>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<!-- Tooltip -->
|
| 252 |
+
<div id="tooltip"></div>
|
| 253 |
+
</div>
|
| 254 |
+
|
| 255 |
+
<!-- SVG Icons -->
|
| 256 |
+
<svg>
|
| 257 |
+
<symbol id="playIcon" viewBox="-2 0 24 24">
|
| 258 |
+
<path
|
| 259 |
+
d="M1 1.98725C1 1.20022 1.87789 0.730421 2.5332 1.16694L14.5605 9.18061C15.146 9.57066 15.146 10.4302 14.5605 10.8203L2.5332 18.833C1.87788 19.2695 1 18.7997 1 18.0126V1.98725Z"
|
| 260 |
+
transform="translate(2 2)"
|
| 261 |
+
/>
|
| 262 |
+
</symbol>
|
| 263 |
+
<symbol id="pauseIcon" viewBox="0 0 20 20">
|
| 264 |
+
<path d="M5 16V4h3v12H5zm7-12h3v12h-3V4z"/>
|
| 265 |
+
</symbol>
|
| 266 |
+
<symbol id="orbitIcon" viewBox="-2 -2 20 20">
|
| 267 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.44728 1.7561C6.14077 0.763957 8.53159 1.57857 10.4375 3.58357C12.75 3.02099 14.4585 3.40647 14.6491 4.73071C14.7831 5.66256 14.139 6.87994 12.9942 8.09008C13.7427 10.7586 13.2413 13.2551 11.5527 14.2444C9.85867 15.2369 7.46679 14.4208 5.56056 12.4143C3.34914 12.9521 1.691 12.6254 1.38412 11.4371L1.35092 11.2698C1.21678 10.3379 1.85972 9.11931 3.00457 7.90909C2.28693 5.34963 2.72037 2.94871 4.24741 1.88435L4.44728 1.7561ZM11.9889 9.03995C11.002 9.88136 9.79998 10.6808 8.47918 11.3303L8.13673 11.4937C7.70533 11.6942 7.28148 11.8681 6.8698 12.0172C7.32387 12.4287 7.79226 12.7587 8.25326 12.9983C9.39237 13.5903 10.3231 13.574 10.946 13.2092C11.5745 12.841 12.0613 12.0184 12.1263 10.7086C12.1518 10.1934 12.1065 9.62999 11.9889 9.03995ZM3.46225 9.20206C3.15128 9.5802 2.91825 9.93412 2.76238 10.2489C2.54363 10.6909 2.51958 10.967 2.53842 11.0985C2.54902 11.1721 2.56635 11.2146 2.67058 11.2763C2.81832 11.3636 3.12484 11.4626 3.6478 11.467C3.95641 11.4696 4.3074 11.4373 4.69402 11.3707C4.45655 11.0439 4.23084 10.6971 4.02215 10.3303C3.81017 9.95774 3.62416 9.58004 3.46225 9.20206ZM10.0358 4.9423C9.42018 5.13611 8.75205 5.40184 8.05014 5.74698C6.61265 6.45384 5.33887 7.34291 4.37175 8.24373C4.55213 8.73829 4.78227 9.2401 5.06511 9.73722C5.33995 10.2202 5.64271 10.6614 5.96355 11.0575C6.5793 10.8637 7.24782 10.5987 7.94988 10.2535C9.38732 9.54667 10.6605 8.65688 11.6276 7.7561C11.4473 7.26173 11.2176 6.76019 10.9349 6.26326C10.66 5.78001 10.3568 5.33855 10.0358 4.9423ZM7.74675 3.00219C6.60759 2.41015 5.67698 2.42644 5.05405 2.79126C4.42548 3.15953 3.93866 3.98199 3.87371 5.29191C3.84822 5.80668 3.89236 6.36967 4.00978 6.95922C4.91091 6.19088 5.9923 5.45862 7.17905 4.84269L7.52084 4.67016C8.07056 4.39986 8.6096 4.17089 9.12957 3.98266C8.67568 3.57138 8.20757 3.24172 7.74675 3.00219ZM12.3522 4.53344C12.0433 4.53086 11.6918 4.56177 11.3047 4.6285C11.5425 4.95566 11.7689 5.3029 11.9779 5.67016C12.1897 6.04255 12.3753 6.41999 12.5371 6.79777C12.8481 6.41965 13.0818 6.06635 13.2376 5.75154C13.4564 5.30973 13.4804 5.03352 13.4616 4.90193C13.451 4.82831 13.4337 4.7859 13.3294 4.7242C13.1817 4.63684 12.8753 4.53787 12.3522 4.53344Z"/>
|
| 268 |
+
</symbol>
|
| 269 |
+
<symbol id="flyIcon" viewBox="-2 -2 20 20">
|
| 270 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 1.33337C13.4727 1.33337 14.6666 2.52728 14.6666 4.00004C14.6666 5.4728 13.4727 6.66671 12 6.66671C11.4931 6.66671 11.0194 6.52491 10.6159 6.27934L10.2721 6.62374C10.0345 6.86142 9.90102 7.18411 9.90102 7.52022V8.47986C9.90102 8.81597 10.0345 9.13866 10.2721 9.37634L10.6159 9.72009C11.0193 9.47463 11.4932 9.33337 12 9.33337C13.4727 9.33337 14.6666 10.5273 14.6666 12C14.6666 13.4728 13.4727 14.6667 12 14.6667C10.5272 14.6667 9.33331 13.4728 9.33331 12C9.33331 11.4932 9.47457 11.0194 9.72003 10.6159L9.37628 10.2722C9.1386 10.0345 8.81591 9.90108 8.4798 9.90108H7.52016C7.18405 9.90108 6.86136 10.0345 6.62368 10.2722L6.27928 10.6159C6.52485 11.0195 6.66665 11.4931 6.66665 12C6.66665 13.4728 5.47274 14.6667 3.99998 14.6667C2.52722 14.6667 1.33331 13.4728 1.33331 12C1.33331 10.5273 2.52722 9.33337 3.99998 9.33337C4.50658 9.33337 4.98008 9.47481 5.38344 9.72009L5.72784 9.37634C5.9655 9.13866 6.09894 8.81597 6.09894 8.47986V7.52022C6.09894 7.18411 5.9655 6.86142 5.72784 6.62374L5.38344 6.27934C4.98001 6.52473 4.5067 6.66671 3.99998 6.66671C2.52722 6.66671 1.33331 5.4728 1.33331 4.00004C1.33331 2.52728 2.52722 1.33337 3.99998 1.33337C5.47274 1.33337 6.66665 2.52728 6.66665 4.00004C6.66665 4.50676 6.52467 4.98008 6.27928 5.3835L6.62368 5.72791C6.86136 5.96556 7.18405 6.099 7.52016 6.099H8.4798C8.81591 6.099 9.1386 5.96556 9.37628 5.72791L9.72003 5.3835C9.47475 4.98014 9.33331 4.50664 9.33331 4.00004C9.33331 2.52728 10.5272 1.33337 12 1.33337ZM3.99998 10.3998C3.11632 10.3998 2.39972 11.1164 2.39972 12C2.39972 12.8837 3.11632 13.6003 3.99998 13.6003C4.88364 13.6003 5.60024 12.8837 5.60024 12C5.60024 11.7911 5.55917 11.5919 5.48631 11.4089L4.41469 12.4812C4.16723 12.7284 3.76625 12.7286 3.51886 12.4812C3.27147 12.2338 3.27158 11.8328 3.51886 11.5853L4.59047 10.5131C4.40769 10.4404 4.20862 10.3998 3.99998 10.3998ZM12 10.3998C11.7911 10.3998 11.5918 10.4403 11.4088 10.5131L12.4811 11.5853C12.7284 11.8328 12.7285 12.2338 12.4811 12.4812C12.2337 12.7286 11.8327 12.7284 11.5853 12.4812L10.513 11.4089C10.4402 11.5918 10.3997 11.7912 10.3997 12C10.3997 12.8837 11.1163 13.6003 12 13.6003C12.8836 13.6003 13.6002 12.8837 13.6002 12C13.6002 11.1164 12.8836 10.3998 12 10.3998ZM3.99998 2.39978C3.11632 2.39978 2.39972 3.11639 2.39972 4.00004C2.39972 4.8837 3.11632 5.6003 3.99998 5.6003C4.20871 5.6003 4.40763 5.55909 4.59047 5.48637L3.51886 4.41475C3.27158 4.16729 3.27147 3.76631 3.51886 3.51892C3.76625 3.27153 4.16723 3.27164 4.41469 3.51892L5.48631 4.59054C5.55903 4.40769 5.60024 4.20877 5.60024 4.00004C5.60024 3.11639 4.88364 2.39978 3.99998 2.39978ZM12 2.39978C11.1163 2.39978 10.3997 3.11639 10.3997 4.00004C10.3997 4.20868 10.4403 4.40775 10.513 4.59054L11.5853 3.51892C11.8327 3.27164 12.2337 3.27153 12.4811 3.51892C12.7285 3.76631 12.7284 4.16729 12.4811 4.41475L11.4088 5.48637C11.5918 5.55923 11.791 5.6003 12 5.6003C12.8836 5.6003 13.6002 4.8837 13.6002 4.00004C13.6002 3.11639 12.8836 2.39978 12 2.39978Z"/>
|
| 271 |
+
</symbol>
|
| 272 |
+
<symbol id="arIcon" viewBox="-2 -6 28 28">
|
| 273 |
+
<path d="M10.9482 16.9199C10.9799 16.9389 11.0201 16.9389 11.0518 16.9199L15.918 14H19.416L11.9775 18.4629C11.3758 18.8239 10.6242 18.8239 10.0225 18.4629L2.58398 14H6.08203L10.9482 16.9199Z"/>
|
| 274 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.041 12H9.40527L8.35742 9.27441H3.56055L2.57031 12H0L4.67383 0H7.23633L12.041 12ZM4.30566 7.25195H7.58008L5.92676 2.7998L4.30566 7.25195Z"/>
|
| 275 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.0039 0C18.2519 0 19.1577 0.108901 19.7207 0.327148C20.289 0.539972 20.7431 0.922473 21.083 1.47363C21.4229 2.02476 21.5928 2.65491 21.5928 3.36426C21.5928 4.26454 21.3353 5.00931 20.8203 5.59863C20.3051 6.18253 19.5348 6.5513 18.5098 6.7041C19.0196 7.00969 19.4393 7.34532 19.7686 7.71094C20.1031 8.07658 20.5523 8.72611 21.1152 9.65918L22.541 12H19.7207L18.0156 9.38867C17.4104 8.45586 16.9965 7.8691 16.7734 7.62891C16.5505 7.38348 16.3139 7.21724 16.0645 7.12988C15.8148 7.03711 15.4187 6.99023 14.877 6.99023H14.3994V12H12.041V0H17.0039ZM14.3994 5.0752H16.1436C17.2746 5.0752 17.9811 5.02592 18.2627 4.92773C18.5442 4.82951 18.7645 4.66003 18.9238 4.41992C19.0831 4.17982 19.1631 3.87966 19.1631 3.51953C19.163 3.11597 19.057 2.79138 18.8447 2.5459C18.6376 2.29488 18.3424 2.13677 17.96 2.07129C17.7687 2.04401 17.1951 2.03027 16.2393 2.03027H14.3994V5.0752Z"/>
|
| 276 |
+
</symbol>
|
| 277 |
+
<symbol id="vrIcon" viewBox="-2 -6 28 28">
|
| 278 |
+
<path d="M7.90039 16C7.90039 16.6075 8.39249 17.0996 9 17.0996H11V18.9004H9C7.39837 18.9004 6.09961 17.6016 6.09961 16V15H7.90039V16Z"/>
|
| 279 |
+
<path d="M16.9004 16C16.9004 17.6016 15.6016 18.9004 14 18.9004H12V17.0996H14C14.6075 17.0996 15.0996 16.6075 15.0996 16V15H16.9004V16Z"/>
|
| 280 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.52148 4.70215L6.15918 9.11328L7.76465 4.70215L8.50195 2.7002L9.4834 0H15.9541C17.1903 0 18.0879 0.108014 18.6455 0.324219C19.2081 0.534989 19.6576 0.913398 19.9941 1.45898C20.3308 2.00484 20.499 2.62946 20.499 3.33203C20.499 4.22386 20.2446 4.96215 19.7344 5.5459C19.2241 6.12424 18.4606 6.48928 17.4453 6.64062C17.9503 6.94331 18.3662 7.27555 18.6924 7.6377C19.0238 7.99986 19.4688 8.64321 20.0264 9.56738L21.4385 11.8857H18.6455L16.9561 9.2998C16.3565 8.3758 15.9466 7.79459 15.7256 7.55664C15.5047 7.31345 15.2706 7.14802 15.0234 7.06152C14.7762 6.96965 14.3841 6.92384 13.8477 6.92383H13.374V11.8857H11.0381V2.54395L7.39941 11.8857H4.86133L0.102539 0H2.71289L4.52148 4.70215ZM13.374 5.02637H15.1025C16.2228 5.02636 16.9224 4.97814 17.2012 4.88086C17.4799 4.78356 17.6986 4.61574 17.8564 4.37793C18.0141 4.14017 18.0928 3.84288 18.0928 3.48633C18.0928 3.08641 17.9877 2.7647 17.7773 2.52148C17.5722 2.27286 17.2801 2.11565 16.9014 2.05078C16.712 2.02376 16.1439 2.01075 15.1973 2.01074H13.374V5.02637Z"/>
|
| 281 |
+
</symbol>
|
| 282 |
+
<symbol id="infoIcon" viewBox="-2 -2 24 24">
|
| 283 |
+
<path d="M9.98633 7.58301C10.2598 7.58301 10.4854 7.67643 10.6631 7.86328C10.8408 8.05013 10.9297 8.3099 10.9297 8.64258V14.0361C10.9297 14.4098 10.8408 14.6924 10.6631 14.8838C10.4854 15.0752 10.2598 15.1709 9.98633 15.1709C9.71289 15.1709 9.48958 15.0729 9.31641 14.877C9.14779 14.681 9.06348 14.4007 9.06348 14.0361V8.69727C9.06348 8.32812 9.14779 8.05013 9.31641 7.86328C9.48958 7.67643 9.71289 7.58301 9.98633 7.58301Z"/>
|
| 284 |
+
<path d="M10.0068 4.88965C10.2484 4.88965 10.4626 4.96712 10.6494 5.12207C10.8363 5.27702 10.9297 5.5026 10.9297 5.79883C10.9297 6.08594 10.8385 6.31152 10.6562 6.47559C10.474 6.63509 10.2575 6.71484 10.0068 6.71484C9.74707 6.71484 9.52376 6.63509 9.33691 6.47559C9.15462 6.31608 9.06348 6.09049 9.06348 5.79883C9.06348 5.53451 9.1569 5.31803 9.34375 5.14941C9.53516 4.97624 9.75618 4.88965 10.0068 4.88965Z"/>
|
| 285 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0ZM10 1.7998C5.47126 1.7998 1.7998 5.47126 1.7998 10C1.7998 14.5287 5.47126 18.2002 10 18.2002C14.5287 18.2002 18.2002 14.5287 18.2002 10C18.2002 5.47126 14.5287 1.7998 10 1.7998Z"/>
|
| 286 |
+
</symbol>
|
| 287 |
+
<symbol id="settingsIcon" viewBox="0 0 24 24">
|
| 288 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 8C14.2091 8 16 9.79086 16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8ZM12 9.7998C10.785 9.7998 9.7998 10.785 9.7998 12C9.7998 13.215 10.785 14.2002 12 14.2002C13.215 14.2002 14.2002 13.215 14.2002 12C14.2002 10.785 13.215 9.7998 12 9.7998Z"/>
|
| 289 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7119 2.2002C13.1961 2.20028 13.6296 2.49055 13.8164 2.93066L13.8506 3.02051L14.3652 4.56641C14.7875 4.70091 15.1932 4.87075 15.5801 5.07129L17.042 4.3418L17.1299 4.30176C17.5436 4.13502 18.017 4.21245 18.3564 4.50195L18.4268 4.56641L19.4336 5.57324C19.7985 5.93836 19.8889 6.49621 19.6582 6.95801L18.9268 8.41895C19.1274 8.80592 19.2971 9.21159 19.4316 9.63379L20.9795 10.1504C21.4693 10.3138 21.7997 10.7717 21.7998 11.2881V12.7119C21.7997 13.2284 21.4695 13.6873 20.9795 13.8506L19.4316 14.3652C19.2971 14.7875 19.1274 15.1932 18.9268 15.5801L19.6582 17.042C19.8889 17.5038 19.7985 18.0616 19.4336 18.4268L18.4268 19.4336C18.0616 19.7985 17.5038 19.8889 17.042 19.6582L15.5801 18.9268C15.1932 19.1274 14.7875 19.2971 14.3652 19.4316L13.8506 20.9795C13.6873 21.4695 13.2284 21.7997 12.7119 21.7998H11.2881C10.7717 21.7997 10.3138 21.4693 10.1504 20.9795L9.63379 19.4316C9.21159 19.2971 8.80592 19.1274 8.41895 18.9268L6.95801 19.6582C6.49621 19.8889 5.93836 19.7985 5.57324 19.4336L4.56641 18.4268C4.20146 18.0617 4.1112 17.5038 4.3418 17.042L5.07129 15.5801C4.87075 15.1932 4.70091 14.7875 4.56641 14.3652L3.02051 13.8506C2.53057 13.6873 2.20029 13.2283 2.2002 12.7119V11.2881C2.20024 10.7718 2.53076 10.3139 3.02051 10.1504L4.56641 9.63379C4.70094 9.21149 4.86966 8.80498 5.07031 8.41797L4.3418 6.95801C4.11113 6.49617 4.20145 5.93834 4.56641 5.57324L5.57324 4.56641C5.93834 4.20145 6.49617 4.11113 6.95801 4.3418L8.41797 5.07031C8.80498 4.86966 9.21149 4.70094 9.63379 4.56641L10.1504 3.02051L10.1836 2.92969C10.3706 2.49001 10.8042 2.20023 11.2881 2.2002H12.7119ZM11.0186 5.4707L10.8809 5.88477L10.458 5.99316C9.88479 6.13982 9.34317 6.36768 8.84473 6.66309L8.46875 6.88477L8.0791 6.69043L6.50098 5.90137L5.90137 6.50098L6.69043 8.0791L6.88477 8.46875L6.66309 8.84473C6.36768 9.34317 6.13982 9.88479 5.99316 10.458L5.88477 10.8809L5.4707 11.0186L3.7998 11.5762V12.4229L5.4707 12.9805L5.88477 13.1182L5.99316 13.541C6.13969 14.1141 6.36763 14.6556 6.66309 15.1543L6.88477 15.5303L6.69043 15.9199L5.90137 17.498L6.50098 18.0977L8.0791 17.3086L8.46875 17.1133L8.84473 17.3359C9.34314 17.6314 9.88463 17.8591 10.458 18.0059L10.8809 18.1143L11.0186 18.5283L11.5771 20.2002H12.4229L13.1182 18.1143L13.541 18.0059C14.1143 17.8593 14.6556 17.6314 15.1543 17.3359L15.5303 17.1143L15.9199 17.3086L17.498 18.0977L18.0977 17.498L17.3086 15.9199L17.1143 15.5303L17.3359 15.1543C17.6314 14.6556 17.8593 14.1143 18.0059 13.541L18.1143 13.1182L20.2002 12.4229V11.5762L18.1143 10.8809L18.0059 10.458C17.8591 9.88463 17.6314 9.34314 17.3359 8.84473L17.1133 8.46875L17.3086 8.0791L18.0977 6.50098L17.498 5.90137L15.9199 6.69043L15.5303 6.88477L15.1543 6.66309C14.6556 6.36763 14.1141 6.13969 13.541 5.99316L13.1182 5.88477L12.9805 5.4707L12.4229 3.7998H11.5762L11.0186 5.4707Z"/>
|
| 290 |
+
</symbol>
|
| 291 |
+
<symbol id="enterFullscreenIcon" viewBox="0 0 24 24">
|
| 292 |
+
<path d="M3 14.7002C3.49693 14.7002 3.90018 15.1027 3.90039 15.5996V20C3.90039 20.0552 3.94477 20.0996 4 20.0996H8.40039C8.89727 20.0998 9.2998 20.5031 9.2998 21C9.2998 21.4969 8.89727 21.9002 8.40039 21.9004H4C2.95066 21.9004 2.09961 21.0493 2.09961 20V15.5996C2.09982 15.1027 2.50307 14.7002 3 14.7002Z"/>
|
| 293 |
+
<path d="M21 14.7002C21.4969 14.7002 21.9002 15.1027 21.9004 15.5996V20C21.9004 21.0493 21.0493 21.9004 20 21.9004H15.5996C15.1027 21.9002 14.7002 21.4969 14.7002 21C14.7002 20.5031 15.1027 20.0998 15.5996 20.0996H20C20.0552 20.0996 20.0996 20.0552 20.0996 20V15.5996C20.0998 15.1027 20.5031 14.7002 21 14.7002Z"/>
|
| 294 |
+
<path d="M8.40039 2.09961C8.89727 2.09982 9.2998 2.50307 9.2998 3C9.2998 3.49693 8.89727 3.90018 8.40039 3.90039H4C3.94477 3.90039 3.90039 3.94477 3.90039 4V8.40039C3.90018 8.89727 3.49693 9.2998 3 9.2998C2.50307 9.2998 2.09982 8.89727 2.09961 8.40039V4C2.09961 2.95066 2.95066 2.09961 4 2.09961H8.40039Z"/>
|
| 295 |
+
<path d="M20 2.09961C21.0493 2.09961 21.9004 2.95066 21.9004 4V8.40039C21.9002 8.89727 21.4969 9.2998 21 9.2998C20.5031 9.2998 20.0998 8.89727 20.0996 8.40039V4C20.0996 3.94477 20.0552 3.90039 20 3.90039H15.5996C15.1027 3.90018 14.7002 3.49693 14.7002 3C14.7002 2.50307 15.1027 2.09982 15.5996 2.09961H20Z"/>
|
| 296 |
+
</symbol>
|
| 297 |
+
<symbol id="exitFullscreenIcon" viewBox="0 0 24 24">
|
| 298 |
+
<path d="M8 15.0996C8.49706 15.0996 8.90039 15.5029 8.90039 16V21C8.90039 21.4971 8.49706 21.9004 8 21.9004C7.50294 21.9004 7.09961 21.4971 7.09961 21V16.9004H3C2.50294 16.9004 2.09961 16.4971 2.09961 16C2.09961 15.5029 2.50294 15.0996 3 15.0996H8Z" />
|
| 299 |
+
<path d="M21 15.0996C21.4971 15.0996 21.9004 15.5029 21.9004 16C21.9004 16.4971 21.4971 16.9004 21 16.9004H16.9004V21C16.9004 21.4971 16.4971 21.9004 16 21.9004C15.5029 21.9004 15.0996 21.4971 15.0996 21V16C15.0996 15.5029 15.5029 15.0996 16 15.0996H21Z" />
|
| 300 |
+
<path d="M8 2.09961C8.49706 2.09961 8.90039 2.50294 8.90039 3V8C8.90039 8.49706 8.49706 8.90039 8 8.90039H3C2.50294 8.90039 2.09961 8.49706 2.09961 8C2.09961 7.50294 2.50294 7.09961 3 7.09961H7.09961V3C7.09961 2.50294 7.50294 2.09961 8 2.09961Z" />
|
| 301 |
+
<path d="M16 2.09961C16.4971 2.09961 16.9004 2.50294 16.9004 3V7.09961H21C21.4971 7.09961 21.9004 7.50294 21.9004 8C21.9004 8.49706 21.4971 8.90039 21 8.90039H16C15.5029 8.90039 15.0996 8.49706 15.0996 8V3C15.0996 2.50294 15.5029 2.09961 16 2.09961Z" />
|
| 302 |
+
</symbol>
|
| 303 |
+
</svg>
|
| 304 |
+
|
| 305 |
+
<!-- Application Script -->
|
| 306 |
+
<script type="module">
|
| 307 |
+
import { main } from './index.js';
|
| 308 |
+
|
| 309 |
+
const { config, settings } = window.sse;
|
| 310 |
+
const { poster } = config;
|
| 311 |
+
|
| 312 |
+
// Show the poster image
|
| 313 |
+
if (poster) {
|
| 314 |
+
const element = document.getElementById('poster');
|
| 315 |
+
element.style.setProperty('--poster-url', `url(${poster.src})`);
|
| 316 |
+
element.style.display = 'block';
|
| 317 |
+
element.style.filter = 'blur(40px)';
|
| 318 |
+
|
| 319 |
+
// hide the canvas
|
| 320 |
+
document.documentElement.style.setProperty('--canvas-opacity', '0');
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 324 |
+
const [appElement, cameraElement, settingsJson] = await Promise.all([
|
| 325 |
+
document.querySelector('pc-app').ready(),
|
| 326 |
+
document.querySelector('pc-entity[name="camera"]').ready(),
|
| 327 |
+
settings
|
| 328 |
+
]);
|
| 329 |
+
|
| 330 |
+
const viewer = await main(appElement.app, cameraElement.entity, settingsJson, config);
|
| 331 |
+
});
|
| 332 |
+
</script>
|
| 333 |
+
</body>
|
| 334 |
+
</html>
|
viewer/index.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
viewer/index.js.map
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
viewer/settings.default.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"background": {
|
| 3 |
+
"color": [
|
| 4 |
+
0,
|
| 5 |
+
0,
|
| 6 |
+
0,
|
| 7 |
+
0
|
| 8 |
+
]
|
| 9 |
+
},
|
| 10 |
+
"camera": {
|
| 11 |
+
"fov": 37.84928883210247,
|
| 12 |
+
"position": [
|
| 13 |
+
0,
|
| 14 |
+
1,
|
| 15 |
+
-1
|
| 16 |
+
],
|
| 17 |
+
"target": [
|
| 18 |
+
0,
|
| 19 |
+
0,
|
| 20 |
+
0
|
| 21 |
+
],
|
| 22 |
+
"startAnim": "none",
|
| 23 |
+
"animTrack": ""
|
| 24 |
+
},
|
| 25 |
+
"animTracks": []
|
| 26 |
+
}
|