File size: 16,550 Bytes
d4e2238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Immersive 3D Gaussian Splat Viewer</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body { margin: 0; overflow: hidden; background-color: #050505; font-family: 'Inter', sans-serif; }
        canvas { display: block; width: 100vw; height: 100vh; outline: none; }
        
        /* Custom Scrollbar for the scene list */
        .scroller::-webkit-scrollbar { height: 6px; }
        .scroller::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); }
        .scroller::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 10px; }
        .scroller::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.4); }

        /* Glassmorphism utilities */
        .glass {
            background: rgba(20, 20, 20, 0.6);
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .loading-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: #000; z-index: 50; display: flex; flex-direction: column;
            align-items: center; justify-content: center; transition: opacity 0.5s ease;
        }
        
        .loader-bar {
            width: 200px; height: 2px; background: #333; margin-top: 20px; position: relative; overflow: hidden;
        }
        .loader-progress {
            position: absolute; top: 0; left: 0; height: 100%; width: 0%; background: #00d2ff; transition: width 0.2s;
        }
    </style>
    <!-- Import Map for Three.js and Addons -->
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
            }
        }
    </script>
</head>
<body class="text-white antialiased selection:bg-cyan-500 selection:text-black">

    <!-- Loading Screen -->
    <div id="loader" class="loading-overlay">
        <div class="text-2xl font-light tracking-widest uppercase text-cyan-400">Initializing Neural Renderer</div>
        <div class="text-xs text-gray-500 mt-2">Loading Gaussian Splats...</div>
        <div class="loader-bar"><div id="progress-bar" class="loader-progress"></div></div>
    </div>

    <!-- Main UI Overlay -->
    <div class="absolute inset-0 pointer-events-none flex flex-col justify-between z-10 p-6">
        
        <!-- Header -->
        <header class="flex justify-between items-start pointer-events-auto">
            <div>
                <h1 class="text-3xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-600">
                    SPLAT<span class="text-white">VIEWER</span>
                </h1>
                <p class="text-xs text-gray-400 mt-1 max-w-xs">
                    Explore photorealistic 3D captures using Gaussian Splatting technology. 
                    <br>Drag to rotate • Scroll to zoom • Right-click to pan.
                </p>
            </div>
            <div class="glass px-4 py-2 rounded-full flex items-center gap-2">
                <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
                <span class="text-xs font-mono text-gray-300">LIVE RENDER</span>
            </div>
        </header>

        <!-- Controls & Info (Right Side) -->
        <div class="absolute top-1/2 right-6 transform -translate-y-1/2 flex flex-col gap-3 pointer-events-auto">
            <button id="btn-reset" class="glass w-10 h-10 rounded-full flex items-center justify-center hover:bg-white/10 transition text-cyan-400" title="Reset Camera">
                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 12"/></svg>
            </button>
            <button id="btn-rotate" class="glass w-10 h-10 rounded-full flex items-center justify-center hover:bg-white/10 transition text-cyan-400 bg-white/5" title="Toggle Auto-Rotation">
                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
            </button>
            <button id="btn-wireframe" class="glass w-10 h-10 rounded-full flex items-center justify-center hover:bg-white/10 transition text-cyan-400" title="Toggle Debug View">
                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
            </button>
        </div>

        <!-- Scene Selector (Bottom) -->
        <div class="w-full max-w-4xl mx-auto pointer-events-auto">
            <div class="glass p-4 rounded-2xl">
                <div class="flex justify-between items-center mb-3">
                    <h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider">Select World</h3>
                    <span id="scene-desc" class="text-xs text-cyan-400 font-mono">Loading...</span>
                </div>
                <div class="scroller flex gap-4 overflow-x-auto pb-2" id="scene-list">
                    <!-- Scene items injected via JS -->
                </div>
            </div>
        </div>
    </div>

    <!-- Canvas Container -->
    <div id="canvas-container" class="absolute inset-0 z-0 bg-black"></div>

    <!-- Application Logic -->
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GaussianSplattingMesh } from 'three/addons/objects/GaussianSplattingMesh.js';

        // --- Configuration ---
        // Using publicly available demo splats from Three.js examples or similar CDNs
        const SCENES = [
            {
                id: 'garden',
                name: 'The Garden',
                description: 'A high-detail capture of a lush garden.',
                // Using a standard demo file from three.js repo (approx 30MB)
                url: 'https://raw.githubusercontent.com/mrdoob/three.js/master/examples/models/gltf/gaussian-splatting/garden.ply',
                cameraPos: { x: 0, y: 1, z: 3 },
                scale: 1,
                splatAlphaRemovalThreshold: 0.1
            },
            {
                id: 'bonsai',
                name: 'Bonsai Tree',
                description: 'Intricate details of a miniature tree.',
                url: 'https://raw.githubusercontent.com/mrdoob/three.js/master/examples/models/gltf/gaussian-splatting/bonsai.ply',
                cameraPos: { x: 1, y: 0.5, z: 1.5 },
                scale: 2,
                splatAlphaRemovalThreshold: 0.05
            },
            {
                id: 'stump',
                name: 'Old Stump',
                description: 'Weathered wood textures in 3D.',
                url: 'https://raw.githubusercontent.com/mrdoob/three.js/master/examples/models/gltf/gaussian-splatting/stump.ply',
                cameraPos: { x: 2, y: 1, z: 2 },
                scale: 1.5,
                splatAlphaRemovalThreshold: 0.1
            },
            {
                id: 'bicycle',
                name: 'Vintage Bike',
                description: 'A classic bicycle reconstruction.',
                url: 'https://raw.githubusercontent.com/mrdoob/three.js/master/examples/models/gltf/gaussian-splatting/bicycle.ply',
                cameraPos: { x: 1, y: 0.5, z: 2 },
                scale: 1.2,
                splatAlphaRemovalThreshold: 0.05
            }
        ];

        // --- State ---
        let camera, scene, renderer, controls;
        let currentMesh = null;
        let isAutoRotating = true;
        let targetSceneIndex = 0;

        // --- DOM Elements ---
        const container = document.getElementById('canvas-container');
        const loader = document.getElementById('loader');
        const progressBar = document.getElementById('progress-bar');
        const sceneListEl = document.getElementById('scene-list');
        const sceneDescEl = document.getElementById('scene-desc');
        const btnReset = document.getElementById('btn-reset');
        const btnRotate = document.getElementById('btn-rotate');
        const btnWireframe = document.getElementById('btn-wireframe');

        // --- Initialization ---
        init();
        buildUI();
        loadScene(0);

        function init() {
            // Scene
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x111111);

            // Camera
            camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(0, 1, 3);

            // Renderer
            renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);
            container.appendChild(renderer.domElement);

            // Controls
            controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.05;
            controls.autoRotate = true;
            controls.autoRotateSpeed = 1.0;
            controls.target.set(0, 0.5, 0);

            // Event Listeners
            window.addEventListener('resize', onWindowResize);
            
            // Animation Loop
            renderer.setAnimationLoop(animate);
        }

        function buildUI() {
            SCENES.forEach((sceneData, index) => {
                const btn = document.createElement('button');
                btn.className = `
                    flex-shrink-0 w-32 h-20 rounded-xl border border-white/10 overflow-hidden relative group transition-all duration-300
                    hover:border-cyan-400 hover:shadow-[0_0_15px_rgba(34,211,238,0.3)]
                    ${index === 0 ? 'ring-2 ring-cyan-400' : 'opacity-70 hover:opacity-100'}
                `;
                
                // Generate a gradient placeholder for the thumbnail since we don't have images
                const hue = (index * 60) % 360;
                btn.innerHTML = `
                    <div class="absolute inset-0 bg-gradient-to-br from-gray-800 to-black group-hover:scale-110 transition-transform duration-500"></div>
                    <div class="absolute inset-0 flex flex-col items-center justify-center z-10">
                        <span class="text-xs font-bold text-white uppercase tracking-widest drop-shadow-md">${sceneData.name}</span>
                    </div>
                    <div class="absolute bottom-0 left-0 w-full h-1 bg-cyan-500 transform scale-x-0 group-hover:scale-x-100 transition-transform origin-left"></div>
                `;
                
                btn.onclick = () => {
                    // Update active state UI
                    Array.from(sceneListEl.children).forEach(c => {
                        c.classList.remove('ring-2', 'ring-cyan-400', 'opacity-100');
                        c.classList.add('opacity-70');
                    });
                    btn.classList.remove('opacity-70');
                    btn.classList.add('ring-2', 'ring-cyan-400', 'opacity-100');
                    
                    loadScene(index);
                };
                sceneListEl.appendChild(btn);
            });
        }

        async function loadScene(index) {
            const data = SCENES[index];
            targetSceneIndex = index;
            
            // Update UI Text
            sceneDescEl.textContent = `Loading: ${data.name}...`;
            
            // Show loader if it's a new load (optional, but good for UX)
            // loader.style.opacity = '1'; 
            // loader.style.pointerEvents = 'all';
            
            try {
                // Remove old mesh
                if (currentMesh) {
                    scene.remove(currentMesh);
                    currentMesh.dispose();
                    currentMesh = null;
                }

                // Create new Gaussian Splat Mesh
                // Note: GaussianSplattingMesh handles the .ply loading internally via a static load method usually,
                // but in newer three.js versions, we might need to load buffer and pass it.
                // Let's check the API. 
                // The class usually expects a URL or buffer.
                
                const mesh = new GaussianSplattingMesh();
                mesh.splatAlphaRemovalThreshold = data.splatAlphaRemovalThreshold;
                
                // Load the PLY file
                await mesh.load(data.url);
                
                // Adjust Scale and Position
                // The raw data might be huge or tiny, we normalize slightly
                mesh.scale.set(data.scale, data.scale, data.scale);
                
                // Center the mesh roughly (some splats are centered, some aren't)
                // Usually Gaussian Splat data is centered around 0,0,0 or needs bounding box calc.
                // For these specific demo files, they are usually centered.
                
                scene.add(mesh);
                currentMesh = mesh;

                // Camera Transition
                // We smoothly move the camera to the predefined position for this scene
                moveCamera(data.cameraPos);
                
                sceneDescEl.textContent = `${data.name}${data.description}`;
                
                // Hide loader
                loader.style.opacity = '0';
                setTimeout(() => { loader.style.pointerEvents = 'none'; }, 500);

            } catch (error) {
                console.error("Error loading splat:", error);
                sceneDescEl.textContent = `Error loading ${data.name}. See console.`;
                loader.style.opacity = '0';
            }
        }

        function moveCamera(targetPos) {
            // Simple interpolation could be added here, but for now direct set with controls update
            // To make it fancy, we could use TWEEN, but let's stick to vanilla/three logic
            camera.position.set(targetPos.x, targetPos.y, targetPos.z);
            controls.target.set(0, 0.2, 0); // Look slightly above origin
            controls.update();
        }

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        function animate() {
            controls.update(); // required if damping enabled
            
            // Subtle floating animation for the mesh if desired, 
            // but splats are usually static scenes.
            
            renderer.render(scene, camera);
        }

        // --- Interaction Logic ---
        
        btnReset.addEventListener('click', () => {
            const data = SCENES[targetSceneIndex];
            moveCamera(data.cameraPos);
        });

        btnRotate.addEventListener('click', () => {
            isAutoRotating = !isAutoRotating;
            controls.autoRotate = isAutoRotating;
            btnRotate.classList.toggle('bg-white/20');
            btnRotate.classList.toggle('text-white');
        });

        btnWireframe.addEventListener('click', () => {
            // Toggle a debug mode or just reset camera for now
            // Gaussian Splatting doesn't have a "wireframe" mode easily accessible without custom shaders
            // So we'll just trigger a flash effect or reset
            scene.background = new THREE.Color(scene.background.getHex() === 0x111111 ? 0x222222 : 0x111111);
        });

        // Initial progress bar simulation
        let p = 0;
        const interval = setInterval(() => {
            p += 5;
            progressBar.style.width = p + '%';
            if(p > 100) clearInterval(interval);
        }, 50);

    </script>
</body>
</html>