Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Visualizer | GraphicsLab</title> | |
| <link rel="icon" type="image/x-icon" href="https://static.photos/technology/32x32/3"> | |
| <link rel="stylesheet" href="style.css"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script> | |
| </head> | |
| <body class="bg-gray-50 text-gray-900"> | |
| <custom-navbar></custom-navbar> | |
| <main class="container mx-auto px-4 py-8"> | |
| <div class="mb-10"> | |
| <h1 class="text-4xl font-bold mb-4">Interactive 3D Visualizer</h1> | |
| <p class="text-gray-600">Load, manipulate, and visualize 3D models with real‑time lighting and effects.</p> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <div class="lg:col-span-2"> | |
| <div class="card p-0 overflow-hidden"> | |
| <div class="bg-gray-900 p-4 flex justify-between items-center"> | |
| <h2 class="text-xl font-bold text-white">WebGL Canvas</h2> | |
| <div class="flex gap-2"> | |
| <button id="playBtn" class="btn-small bg-green-600"> | |
| <i class="fas fa-play mr-1"></i> Play | |
| </button> | |
| <button id="resetBtn" class="btn-small bg-gray-700"> | |
| <i class="fas fa-redo mr-1"></i> Reset | |
| </button> | |
| </div> | |
| </div> | |
| <div id="canvasContainer" class="relative w-full h-96 bg-gradient-to-br from-gray-900 to-black"> | |
| <canvas id="threeCanvas" class="w-full h-full"></canvas> | |
| <div id="loading" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-75 text-white text-xl"> | |
| <div class="text-center"> | |
| <i class="fas fa-spinner fa-spin text-4xl mb-4"></i> | |
| <p>Loading WebGL scene...</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-4 bg-gray-100 flex justify-between items-center"> | |
| <div class="text-sm text-gray-700"> | |
| <span id="fpsCounter">FPS: 60</span> | | |
| <span id="triangleCount">Triangles: 0</span> | | |
| <span id="renderTime">Render: 0ms</span> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button class="text-sm bg-gray-300 hover:bg-gray-400 px-3 py-1 rounded" onclick="toggleWireframe()"> | |
| <i class="fas fa-th"></i> Wireframe | |
| </button> | |
| <button class="text-sm bg-gray-300 hover:bg-gray-400 px-3 py-1 rounded" onclick="toggleShadows()"> | |
| <i class="fas fa-sun"></i> Shadows | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card mt-8"> | |
| <h2 class="text-2xl font-bold mb-4">Model Library</h2> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <div class="model-card p-4 border border-gray-300 rounded-lg text-center cursor-pointer hover:bg-gray-50" onclick="loadModel('sphere')"> | |
| <div class="text-4xl text-blue-500 mb-2"> | |
| <i class="fas fa-globe-americas"></i> | |
| </div> | |
| <div class="font-semibold">Sphere</div> | |
| <div class="text-sm text-gray-500">1,280 triangles</div> | |
| </div> | |
| <div class="model-card p-4 border border-gray-300 rounded-lg text-center cursor-pointer hover:bg-gray-50" onclick="loadModel('cube')"> | |
| <div class="text-4xl text-purple-500 mb-2"> | |
| <i class="fas fa-cube"></i> | |
| </div> | |
| <div class="font-semibold">Cube</div> | |
| <div class="text-sm text-gray-500">12 triangles</div> | |
| </div> | |
| <div class="model-card p-4 border border-gray-300 rounded-lg text-center cursor-pointer hover:bg-gray-50" onclick="loadModel('torus')"> | |
| <div class="text-4xl text-green-500 mb-2"> | |
| <i class="fas fa-ring"></i> | |
| </div> | |
| <div class="font-semibold">Torus</div> | |
| <div class="text-sm text-gray-500">4,800 triangles</div> | |
| </div> | |
| <div class="model-card p-4 border border-gray-300 rounded-lg text-center cursor-pointer hover:bg-gray-50" onclick="loadModel('teapot')"> | |
| <div class="text-4xl text-red-500 mb-2"> | |
| <i class="fas fa-mug-hot"></i> | |
| </div> | |
| <div class="font-semibold">Teapot</div> | |
| <div class="text-sm text-gray-500">6,324 triangles</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="card mb-8"> | |
| <h2 class="text-2xl font-bold mb-4">Controls</h2> | |
| <div class="space-y-6"> | |
| <div> | |
| <label class="block text-gray-700 mb-2">Rotation Speed</label> | |
| <input type="range" min="0" max="10" value="2" class="w-full" id="rotationSpeed" oninput="updateRotation(this.value)"> | |
| <div class="flex justify-between text-sm text-gray-500"> | |
| <span>Slow</span> | |
| <span id="speedValue">2</span> | |
| <span>Fast</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-2">Light Intensity</label> | |
| <input type="range" min="0" max="5" step="0.1" value="1.5" class="w-full" id="lightIntensity" oninput="updateLight(this.value)"> | |
| <div class="flex justify-between text-sm text-gray-500"> | |
| <span>Dim</span> | |
| <span id="lightValue">1.5</span> | |
| <span>Bright</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-2">Background Color</label> | |
| <div class="flex gap-2"> | |
| <button class="flex-1 py-2 rounded bg-gray-800 text-white" onclick="setBackground('#1f2937')">Dark</button> | |
| <button class="flex-1 py-2 rounded bg-gray-300" onclick="setBackground('#f3f4f6')">Light</button> | |
| <button class="flex-1 py-2 rounded bg-blue-900 text-white" onclick="setBackground('#1e3a8a')">Blue</button> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-2">Post‑Processing</label> | |
| <div class="space-y-2"> | |
| <label class="flex items-center"> | |
| <input type="checkbox" class="mr-2" id="bloom" onchange="toggleEffect('bloom')"> | |
| <span>Bloom</span> | |
| </label> | |
| <label class="flex items-center"> | |
| <input type="checkbox" class="mr-2" id="vignette" onchange="toggleEffect('vignette')"> | |
| <span>Vignette</span> | |
| </label> | |
| <label class="flex items-center"> | |
| <input type="checkbox" class="mr-2" id="ssao" onchange="toggleEffect('ssao')"> | |
| <span>SSAO</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2 class="text-2xl font-bold mb-4">Scene Info</h2> | |
| <div class="space-y-3"> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">Renderer</span> | |
| <span class="font-semibold" id="rendererInfo">WebGL 2</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">Draw Calls</span> | |
| <span class="font-semibold" id="drawCalls">1</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">Textures</span> | |
| <span class="font-semibold" id="textureCount">3</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">Lights</span> | |
| <span class="font-semibold" id="lightCount">2</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-600">VRAM Usage</span> | |
| <span class="font-semibold" id="vramUsage">~120 MB</span> | |
| </div> | |
| </div> | |
| <button class="btn-primary w-full mt-6" onclick="exportScene()"> | |
| <i class="fas fa-download mr-2"></i> Export as GLB | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <custom-footer></custom-footer> | |
| <script src="components/navbar.js"></script> | |
| <script src="components/footer.js"></script> | |
| <script src="script.js"></script> | |
| <script> | |
| // Three.js scene variables | |
| let scene, camera, renderer, controls, cube; | |
| let rotationSpeed = 2; | |
| let lightIntensity = 1.5; | |
| let frameCount = 0; | |
| let lastTime = performance.now(); | |
| let fps = 60; | |
| // Initialize Three.js scene | |
| function initScene() { | |
| const canvas = document.getElementById('threeCanvas'); | |
| const container = document.getElementById('canvasContainer'); | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x1f2937); | |
| camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000); | |
| camera.position.z = 5; | |
| renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| // OrbitControls | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| // Lighting | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, lightIntensity); | |
| directionalLight.position.set(5, 5, 5); | |
| directionalLight.castShadow = true; | |
| scene.add(directionalLight); | |
| // Add a default cube | |
| const geometry = new THREE.BoxGeometry(); | |
| const material = new THREE.MeshStandardMaterial({ color: 0x3b82f6, roughness: 0.3, metalness: 0.8 }); | |
| cube = new THREE.Mesh(geometry, material); | |
| cube.castShadow = true; | |
| cube.receiveShadow = true; | |
| scene.add(cube); | |
| // Add a ground plane | |
| const planeGeometry = new THREE.PlaneGeometry(10, 10); | |
| const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x374151, side: THREE.DoubleSide }); | |
| const plane = new THREE.Mesh(planeGeometry, planeMaterial); | |
| plane.rotation.x = Math.PI / 2; | |
| plane.position.y = -2; | |
| plane.receiveShadow = true; | |
| scene.add(plane); | |
| // Hide loading | |
| document.getElementById('loading').style.display = 'none'; | |
| // Start animation loop | |
| animate(); | |
| // Update info | |
| updateInfo(); | |
| } | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| cube.rotation.x += 0.01 * rotationSpeed; | |
| cube.rotation.y += 0.01 * rotationSpeed; | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| // FPS calculation | |
| frameCount++; | |
| const now = performance.now(); | |
| if (now >= lastTime + 1000) { | |
| fps = Math.round((frameCount * 1000) / (now - lastTime)); | |
| frameCount = 0; | |
| lastTime = now; | |
| document.getElementById('fpsCounter').textContent = `FPS: ${fps}`; | |
| } | |
| } | |
| // UI functions | |
| function updateRotation(val) { | |
| rotationSpeed = parseFloat(val); | |
| document.getElementById('speedValue').textContent = val; | |
| } | |
| function updateLight(val) { | |
| lightIntensity = parseFloat(val); | |
| scene.children.forEach(child => { | |
| if (child.isLight) child.intensity = lightIntensity; | |
| }); | |
| document.getElementById('lightValue').textContent = val; | |
| } | |
| function setBackground(color) { | |
| scene.background = new THREE.Color(color); | |
| } | |
| function toggleWireframe() { | |
| cube.material.wireframe = !cube.material.wireframe; | |
| } | |
| function toggleShadows() { | |
| renderer.shadowMap.enabled = !renderer.shadowMap.enabled; | |
| } | |
| function toggleEffect(effect) { | |
| alert(`Effect ${effect} toggled (simulated)`); | |
| } | |
| function loadModel(model) { | |
| alert(`Loading ${model} model (simulated)`); | |
| // In a real app, load GLTF/OBJ here | |
| document.getElementById('triangleCount').textContent = `Triangles: ${{ | |
| sphere: 1280, | |
| cube: 12, | |
| torus: 4800, | |
| teapot: 6324 | |
| }[model]}`; | |
| } | |
| function exportScene() { | |
| alert('Scene exported as GLB (simulated)'); | |
| } | |
| function updateInfo() { | |
| document.getElementById('rendererInfo').textContent = renderer.capabilities.isWebGL2 ? 'WebGL 2' : 'WebGL 1'; | |
| document.getElementById('drawCalls').textContent = renderer.info.render.calls; | |
| document.getElementById('textureCount').textContent = renderer.info.memory.textures; | |
| document.getElementById('lightCount').textContent = scene.children.filter(c => c.isLight).length; | |
| } | |
| // Resize handler | |
| window.addEventListener('resize', () => { | |
| const container = document.getElementById('canvasContainer'); | |
| camera.aspect = container.clientWidth / container.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| }); | |
| // Initialize on load | |
| window.addEventListener('DOMContentLoaded', () => { | |
| initScene(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |