This prototype series focusses on realtime webcam pixel tracking. Pixel color information is used to steer simple 3D objects.
This prototype series focusses on realtime webcam pixel tracking. Pixel color information is used to steer simple 3D objects.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Webcam Voxel Matrix</title> <style> body { margin: 0; overflow: hidden; font-family: 'Inter', sans-serif; background: #111; color: #eee; } canvas { display: block; } .ui-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; pointer-events: none; text-align: center; z-index: 10; background: rgba(17, 17, 17, 0.9); transition: opacity 1s ease-in-out; } .ui-container.controls { top: 20px; left: 20px; width: auto; height: auto; padding: 20px; background: rgba(17, 17, 17, 0.7); border-radius: 12px; flex-direction: column; align-items: flex-start; } .hidden { opacity: 0; pointer-events: none; } button { pointer-events: auto; background-color: #4CAF50; border: none; color: white; padding: 15px 32px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin-top: 20px; cursor: pointer; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.3s ease; } button:hover { background-color: #45a049; transform: translateY(-2px); box-shadow: 0 6px 8px rgba(0,0,0,0.2); } h1 { font-size: 2.5rem; margin-bottom: 0.5rem; } p { font-size: 1.2rem; max-width: 80%; } #webcam-video, #webcam-canvas { display: none; } .slider-group { display: flex; flex-direction: column; align-items: flex-start; margin-bottom: 15px; pointer-events: auto; } .slider-group label { margin-bottom: 5px; font-size: 1rem; text-shadow: 1px 1px 2px #000; } .slider-group input[type="range"] { width: 200px; -webkit-appearance: none; background: #555; border-radius: 5px; outline: none; opacity: 0.7; transition: opacity .2s; } .slider-group input[type="range"]:hover { opacity: 1; } .slider-group input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 15px; height: 15px; background: #4CAF50; border-radius: 50%; cursor: pointer; } </style> </head> <body> <div id="ui-overlay" class="ui-container"> <h1>Webcam Voxel Matrix</h1> <p>This app visualizes your webcam feed as a 3D grid of cubes. Click 'Start Webcam' and grant camera permission to begin.</p> <button id="start-webcam-btn">Start Webcam</button> </div> <div id="control-panel" class="ui-container controls hidden"> <h2>Controls</h2> <div class="slider-group"> <label for="scale-amount-slider">Scale Amount</label> <input type="range" id="scale-amount-slider" min="0.1" max="5.0" step="0.1" value="1.5"> </div> <div class="slider-group"> <label for="z-offset-slider">Z-Offset</label> <input type="range" id="z-offset-slider" min="0.0" max="40.0" step="0.1" value="20.0"> </div> <div class="slider-group"> <label for="position-smoothing-slider">Position Smoothing</label> <input type="range" id="position-smoothing-slider" min="0.01" max="0.5" step="0.01" value="0.05"> </div> <div class="slider-group"> <label for="rotation-smoothing-slider">Rotation Smoothing</label> <input type="range" id="rotation-smoothing-slider" min="0.01" max="0.5" step="0.01" value="0.02"> </div> </div> <video id="webcam-video" autoplay playsinline></video> <canvas id="webcam-canvas"></canvas> <!-- Three.js Library --> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <!-- OrbitControls for camera interaction --> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> <script> // Global variables for Three.js let scene, camera, renderer, controls; let cubes = []; const CUBE_COUNT_X = 128; const CUBE_COUNT_Y = 64; const CUBE_SPACING = 1; // Webcam and canvas variables let video, webcamCanvas, webcamContext; // UI Control variables let scaleAmountMultiplier = 1.5; let zOffsetMultiplier = 20.0; let positionSmoothingFactor = 0.05; let rotationSmoothingFactor = 0.02; // Smoothing variables let smoothedData = []; // Initialize the 3D scene function initThreeJS() { // Scene scene = new THREE.Scene(); // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 100; // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x111111, 1); document.body.appendChild(renderer.domElement); // Controls controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.25; controls.enableZoom = true; // Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); const pointLight = new THREE.PointLight(0xffffff, 0.5); pointLight.position.set(0, 50, 50); scene.add(pointLight); // Create the cubes const geometry = new THREE.BoxGeometry(CUBE_SPACING, CUBE_SPACING, CUBE_SPACING); const material = new THREE.MeshNormalMaterial(); const totalWidth = CUBE_COUNT_X * CUBE_SPACING; const totalHeight = CUBE_COUNT_Y * CUBE_SPACING; for (let y = 0; y < CUBE_COUNT_Y; y++) { for (let x = 0; x < CUBE_COUNT_X; x++) { const cube = new THREE.Mesh(geometry, material); // Center the grid and flip the Y-axis cube.position.x = x * CUBE_SPACING - totalWidth / 2 + CUBE_SPACING / 2; cube.position.y = (CUBE_COUNT_Y - 1 - y) * CUBE_SPACING - totalHeight / 2 + CUBE_SPACING / 2; scene.add(cube); cubes.push(cube); // Initialize smoothed data for each cube smoothedData.push({ scale: new THREE.Vector3(1, 1, 1), rotation: new THREE.Vector3(0, 0, 0) }); } } camera.position.z = Math.max(totalWidth, totalHeight) * 1.5; } // Initialize webcam async function initWebcam() { video = document.getElementById('webcam-video'); webcamCanvas = document.getElementById('webcam-canvas'); webcamCanvas.width = CUBE_COUNT_X; webcamCanvas.height = CUBE_COUNT_Y; webcamContext = webcamCanvas.getContext('2d', { willReadFrequently: true }); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: CUBE_COUNT_X, height: CUBE_COUNT_Y } }); video.srcObject = stream; video.onloadedmetadata = () => { video.play(); document.getElementById('ui-overlay').classList.add('hidden'); document.getElementById('control-panel').classList.remove('hidden'); animate(); // Start the animation loop once video is ready }; } catch (err) { console.error("Error accessing the webcam: ", err); const overlay = document.getElementById('ui-overlay'); overlay.innerHTML = ` <h1>Error</h1> <p>Could not access webcam. Please ensure you have a camera connected and grant permission.</p> `; } } // Animation loop function animate() { requestAnimationFrame(animate); // Update controls controls.update(); if (video && video.readyState === video.HAVE_ENOUGH_DATA) { // Draw video frame to the hidden canvas webcamContext.drawImage(video, 0, 0, CUBE_COUNT_X, CUBE_COUNT_Y); const imageData = webcamContext.getImageData(0, 0, CUBE_COUNT_X, CUBE_COUNT_Y).data; // Update cube properties based on pixel data for (let i = 0; i < cubes.length; i++) { const cube = cubes[i]; const pixelIndex = i * 4; const r = imageData[pixelIndex]; const g = imageData[pixelIndex + 1]; const b = imageData[pixelIndex + 2]; // Brightness (average of RGB values) const brightness = (r + g + b) / 3; // Map brightness to cube size and Z-position const normalizedBrightness = brightness / 255; const newScale = normalizedBrightness * scaleAmountMultiplier + 0.1; const newZPosition = (normalizedBrightness - 0.5) * zOffsetMultiplier; // Apply smoothing to scale and Z-position smoothedData[i].scale.x += (newScale - smoothedData[i].scale.x) * positionSmoothingFactor; smoothedData[i].scale.y += (newScale - smoothedData[i].scale.y) * positionSmoothingFactor; smoothedData[i].scale.z += (newScale - smoothedData[i].scale.z) * positionSmoothingFactor; cube.scale.copy(smoothedData[i].scale); cube.position.z += (newZPosition - cube.position.z) * positionSmoothingFactor; // Map color channels to rotation (scaled from 0 to 2*PI) const newRotationX = (r / 255) * Math.PI * 2; const newRotationY = (g / 255) * Math.PI * 2; const newRotationZ = (b / 255) * Math.PI * 2; // Apply smoothing twice to rotation smoothedData[i].rotation.x += (newRotationX - smoothedData[i].rotation.x) * rotationSmoothingFactor; smoothedData[i].rotation.y += (newRotationY - smoothedData[i].rotation.y) * rotationSmoothingFactor; smoothedData[i].rotation.z += (newRotationZ - smoothedData[i].rotation.z) * rotationSmoothingFactor; // Second smoothing pass cube.rotation.x += (smoothedData[i].rotation.x - cube.rotation.x) * rotationSmoothingFactor; cube.rotation.y += (smoothedData[i].rotation.y - cube.rotation.y) * rotationSmoothingFactor; cube.rotation.z += (smoothedData[i].rotation.z - cube.rotation.z) * rotationSmoothingFactor; } } renderer.render(scene, camera); } // Handle window resizing window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }, false); // Event listeners for UI controls document.getElementById('start-webcam-btn').addEventListener('click', () => { initThreeJS(); initWebcam(); }); document.getElementById('scale-amount-slider').addEventListener('input', (e) => { scaleAmountMultiplier = parseFloat(e.target.value); }); document.getElementById('z-offset-slider').addEventListener('input', (e) => { zOffsetMultiplier = parseFloat(e.target.value); }); document.getElementById('position-smoothing-slider').addEventListener('input', (e) => { positionSmoothingFactor = parseFloat(e.target.value); }); document.getElementById('rotation-smoothing-slider').addEventListener('input', (e) => { rotationSmoothingFactor = parseFloat(e.target.value); }); </script> </body> </html>
FULL CODE ONLINE: https://codepen.io/Tristan-Schulze/pen/YPyMyEv
COMPUTE SHADER UPGRADE
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Webcam Voxel Matrix</title> <style> body { margin: 0; overflow: hidden; font-family: 'Inter', sans-serif; background: #111; color: #eee; } canvas { display: block; } .ui-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; pointer-events: none; text-align: center; z-index: 10; background: rgba(17, 17, 17, 0.9); transition: opacity 1s ease-in-out; } .ui-container.controls { top: 20px; left: 20px; width: auto; height: auto; padding: 20px; background: rgba(17, 17, 17, 0.7); border-radius: 12px; flex-direction: column; align-items: flex-start; } .hidden { opacity: 0; pointer-events: none; } button { pointer-events: auto; background-color: #4CAF50; border: none; color: white; padding: 15px 32px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin-top: 20px; cursor: pointer; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.3s ease; } button:hover { background-color: #45a049; transform: translateY(-2px); box-shadow: 0 6px 8px rgba(0,0,0,0.2); } h1 { font-size: 2.5rem; margin-bottom: 0.5rem; } p { font-size: 1.2rem; max-width: 80%; } #webcam-video, #webcam-canvas { display: none; } .slider-group { display: flex; flex-direction: column; align-items: flex-start; margin-bottom: 15px; pointer-events: auto; } .slider-group label { margin-bottom: 5px; font-size: 1rem; text-shadow: 1px 1px 2px #000; } .slider-group input[type="range"] { width: 200px; -webkit-appearance: none; background: #555; border-radius: 5px; outline: none; opacity: 0.7; transition: opacity .2s; } .slider-group input[type="range"]:hover { opacity: 1; } .slider-group input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 15px; height: 15px; background: #4CAF50; border-radius: 50%; cursor: pointer; } </style> </head> <body> <!-- Overlay for initial instructions --> <div id="ui-overlay" class="ui-container"> <h1>Webcam Voxel Matrix</h1> <p>This app visualizes your webcam feed as a 3D grid of cubes. Click 'Start Webcam' and grant camera permission to begin.</p> <button id="start-webcam-btn">Start Webcam</button> </div> <!-- Control panel for sliders --> <div id="control-panel" class="ui-container controls hidden"> <h2>Controls</h2> <div class="slider-group"> <label for="scale-amount-slider">Scale Amount</label> <input type="range" id="scale-amount-slider" min="0.1" max="5.0" step="0.1" value="1.5"> </div> <div class="slider-group"> <label for="z-offset-slider">Z-Offset</label> <input type="range" id="z-offset-slider" min="0.0" max="40.0" step="0.1" value="20.0"> </div> <div class="slider-group"> <label for="position-smoothing-slider">Position Smoothing</label> <input type="range" id="position-smoothing-slider" min="0.01" max="0.5" step="0.01" value="0.05"> </div> <div class="slider-group"> <label for="rotation-smoothing-slider">Rotation Smoothing</label> <input type="range" id="rotation-smoothing-slider" min="0.01" max="0.5" step="0.01" value="0.02"> </div> <div class="slider-group"> <label for="light-intensity-slider">Light Intensity</label> <input type="range" id="light-intensity-slider" min="0.1" max="2.0" step="0.1" value="1.0"> </div> <div class="slider-group"> <label for="light-direction-slider">Light Direction</label> <input type="range" id="light-direction-slider" min="0" max="360" step="1" value="45"> </div> </div> <!-- Hidden video and canvas for webcam feed processing --> <video id="webcam-video" autoplay playsinline></video> <canvas id="webcam-canvas"></canvas> <!-- Three.js Library --> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <!-- OrbitControls for camera interaction --> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> <script> // Global variables for Three.js scene, camera, and renderer let scene, camera, renderer, controls; let instancedMesh; let dummy = new THREE.Object3D(); // Webcam and canvas variables for video frame processing let video, webcamCanvas, webcamContext; // Voxel grid resolution and number of instances const CUBE_COUNT_X = 256; const CUBE_COUNT_Y = 128; const NUM_INSTANCES = CUBE_COUNT_X * CUBE_COUNT_Y; const CUBE_SPACING = 1.0; // UI Control variables, initialized with default values let scaleAmountMultiplier = 1.5; let zOffsetMultiplier = 20.0; let positionSmoothingFactor = 0.05; let rotationSmoothingFactor = 0.02; let lightIntensity = 1.0; let lightDirectionAngle = 45; // Array to store smoothed data for each cube let smoothedData = []; for(let i = 0; i < NUM_INSTANCES; i++) { smoothedData.push({ scale: new THREE.Vector3(1, 1, 1), rotation: new THREE.Euler(0, 0, 0), zPos: 0 }); } // Lighting let ambientLight; let directionalLight; // Function to create and position the cubes in the scene function createAndPositionCubes() { // Remove previous instanced mesh if it exists to prevent memory leaks if (instancedMesh) { scene.remove(instancedMesh); instancedMesh.geometry.dispose(); instancedMesh.material.dispose(); } // Create the cubes using InstancedMesh for high performance const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshLambertMaterial({ color: 0xffffff }); instancedMesh = new THREE.InstancedMesh(geometry, material, NUM_INSTANCES); scene.add(instancedMesh); const totalWidth = CUBE_COUNT_X * CUBE_SPACING; const totalHeight = CUBE_COUNT_Y * CUBE_SPACING; // Loop through the grid and set the initial position for each cube for (let y = 0; y < CUBE_COUNT_Y; y++) { for (let x = 0; x < CUBE_COUNT_X; x++) { const i = y * CUBE_COUNT_X + x; dummy.position.x = x * CUBE_SPACING - totalWidth / 2 + CUBE_SPACING / 2; dummy.position.y = (CUBE_COUNT_Y - 1 - y) * CUBE_SPACING - totalHeight / 2 + CUBE_SPACING / 2; dummy.position.z = 0; dummy.rotation.set(0, 0, 0); dummy.scale.set(1, 1, 1); dummy.updateMatrix(); instancedMesh.setMatrixAt(i, dummy.matrix); } } instancedMesh.instanceMatrix.needsUpdate = true; camera.position.z = Math.max(totalWidth, totalHeight) * 1.5; camera.updateProjectionMatrix(); } // Initialize the main Three.js scene and controls function initThreeJS() { // Scene scene = new THREE.Scene(); // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); // Renderer if (!renderer) { renderer = new THREE.WebGLRenderer({ antialias: true }); document.body.appendChild(renderer.domElement); } renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x111111, 1); // Controls if (!controls) { controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.25; controls.enableZoom = true; } // Lighting ambientLight = new THREE.AmbientLight(0xffffff, 0.2); scene.add(ambientLight); directionalLight = new THREE.DirectionalLight(0xffffff, lightIntensity); directionalLight.position.set(100, 100, 100); scene.add(directionalLight); // Create and position cubes initially createAndPositionCubes(); } // Initialize the webcam and start the video stream async function initWebcam() { video = document.getElementById('webcam-video'); webcamCanvas = document.getElementById('webcam-canvas'); webcamCanvas.width = CUBE_COUNT_X; webcamCanvas.height = CUBE_COUNT_Y; webcamContext = webcamCanvas.getContext('2d', { willReadFrequently: true }); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: CUBE_COUNT_X, height: CUBE_COUNT_Y } }); video.srcObject = stream; video.onloadedmetadata = () => { video.play(); // Hide the UI overlay and show the controls once the webcam is ready document.getElementById('ui-overlay').classList.add('hidden'); document.getElementById('control-panel').classList.remove('hidden'); animate(); // Start the animation loop }; } catch (err) { console.error("Error accessing the webcam: ", err); const overlay = document.getElementById('ui-overlay'); overlay.innerHTML = ` <h1>Error</h1> <p>Could not access webcam. Please ensure you have a camera connected and grant permission.</p> `; } } // The main animation loop for the scene function animate() { requestAnimationFrame(animate); // Update OrbitControls controls.update(); // Update directional light properties from sliders directionalLight.intensity = lightIntensity; const angle = lightDirectionAngle * Math.PI / 180; directionalLight.position.x = Math.sin(angle) * 150; directionalLight.position.z = Math.cos(angle) * 150; if (video && video.readyState === video.HAVE_ENOUGH_DATA) { // Draw a frame from the video to the hidden 2D canvas webcamContext.drawImage(video, 0, 0, CUBE_COUNT_X, CUBE_COUNT_Y); const imageData = webcamContext.getImageData(0, 0, CUBE_COUNT_X, CUBE_COUNT_Y).data; // Process each pixel and update the corresponding cube for (let y = 0; y < CUBE_COUNT_Y; y++) { for (let x = 0; x < CUBE_COUNT_X; x++) { const i = y * CUBE_COUNT_X + x; const pixelIndex = i * 4; const r = imageData[pixelIndex]; const g = imageData[pixelIndex + 1]; const b = imageData[pixelIndex + 2]; // Calculate brightness (average of RGB) const brightness = (r + g + b) / 3; // Map brightness to cube size and Z-position const normalizedBrightness = brightness / 255; const newScale = normalizedBrightness * scaleAmountMultiplier + 0.1; const newZPosition = (normalizedBrightness - 0.5) * zOffsetMultiplier; // Apply smoothing to the new values smoothedData[i].scale.x += (newScale - smoothedData[i].scale.x) * positionSmoothingFactor; smoothedData[i].scale.y += (newScale - smoothedData[i].scale.y) * positionSmoothingFactor; smoothedData[i].scale.z += (newScale - smoothedData[i].scale.z) * positionSmoothingFactor; smoothedData[i].zPos += (newZPosition - smoothedData[i].zPos) * positionSmoothingFactor; // Map color channels to rotation angles const newRotationX = (r / 255) * Math.PI * 2; const newRotationY = (g / 255) * Math.PI * 2; const newRotationZ = (b / 255) * Math.PI * 2; // Apply smoothing twice for a more gradual rotation change smoothedData[i].rotation.x += (newRotationX - smoothedData[i].rotation.x) * rotationSmoothingFactor; smoothedData[i].rotation.y += (newRotationY - smoothedData[i].rotation.y) * rotationSmoothingFactor; smoothedData[i].rotation.z += (newRotationZ - smoothedData[i].rotation.z) * rotationSmoothingFactor; // Update the dummy object with new values dummy.position.x = x * CUBE_SPACING - (CUBE_COUNT_X * CUBE_SPACING) / 2 + CUBE_SPACING / 2; dummy.position.y = (CUBE_COUNT_Y - 1 - y) * CUBE_SPACING - (CUBE_COUNT_Y * CUBE_SPACING) / 2 + CUBE_SPACING / 2; dummy.position.z = smoothedData[i].zPos; dummy.scale.copy(smoothedData[i].scale); dummy.rotation.set( smoothedData[i].rotation.x + (newRotationX - smoothedData[i].rotation.x) * rotationSmoothingFactor, smoothedData[i].rotation.y + (newRotationY - smoothedData[i].rotation.y) * rotationSmoothingFactor, smoothedData[i].rotation.z + (newRotationZ - smoothedData[i].rotation.z) * rotationSmoothingFactor ); dummy.updateMatrix(); instancedMesh.setMatrixAt(i, dummy.matrix); } } // Tell Three.js to update the instanced mesh with the new matrices instancedMesh.instanceMatrix.needsUpdate = true; } // Render the scene renderer.render(scene, camera); } // Handle window resizing window.addEventListener('resize', () => { if (camera) { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } }, false); // Event listener for the "Start Webcam" button document.getElementById('start-webcam-btn').addEventListener('click', () => { initThreeJS(); initWebcam(); }); // Event listeners for the UI sliders document.getElementById('scale-amount-slider').addEventListener('input', (e) => { scaleAmountMultiplier = parseFloat(e.target.value); }); document.getElementById('z-offset-slider').addEventListener('input', (e) => { zOffsetMultiplier = parseFloat(e.target.value); }); document.getElementById('position-smoothing-slider').addEventListener('input', (e) => { positionSmoothingFactor = parseFloat(e.target.value); }); document.getElementById('rotation-smoothing-slider').addEventListener('input', (e) => { rotationSmoothingFactor = parseFloat(e.target.value); }); document.getElementById('light-intensity-slider').addEventListener('input', (e) => { lightIntensity = parseFloat(e.target.value); }); document.getElementById('light-direction-slider').addEventListener('input', (e) => { lightDirectionAngle = parseFloat(e.target.value); }); </script> </body> </html>
COMPUTE SHADING + FOG + BLOOM
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Webcam Voxel Matrix</title> <style> body { margin: 0; overflow: hidden; font-family: 'Inter', sans-serif; background: #111; color: #eee; } canvas { display: block; } .ui-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; pointer-events: none; text-align: center; z-index: 10; background: rgba(17, 17, 17, 0.9); transition: opacity 1s ease-in-out; } .ui-container.controls { top: 20px; left: 20px; width: auto; height: auto; padding: 20px; background: rgba(17, 17, 17, 0.7); border-radius: 12px; flex-direction: column; align-items: flex-start; } .hidden { opacity: 0; pointer-events: none; } .ui-button { pointer-events: auto; background-color: #4CAF50; border: none; color: white; padding: 15px 32px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin-top: 20px; cursor: pointer; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.3s ease; } .ui-button:hover { background-color: #45a049; transform: translateY(-2px); box-shadow: 0 6px 8px rgba(0,0,0,0.2); } h1 { font-size: 2.5rem; margin-bottom: 0.5rem; } p { font-size: 1.2rem; max-width: 80%; } #webcam-video, #webcam-canvas { display: none; } .slider-group { display: flex; flex-direction: column; align-items: flex-start; margin-bottom: 15px; pointer-events: auto; } .slider-group label { margin-bottom: 5px; font-size: 1rem; text-shadow: 1px 1px 2px #000; } .slider-group input[type="range"] { width: 200px; -webkit-appearance: none; background: #555; border-radius: 5px; outline: none; opacity: 0.7; transition: opacity .2s; } .slider-group input[type="range"]:hover { opacity: 1; } .slider-group input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 15px; height: 15px; background: #4CAF50; border-radius: 50%; cursor: pointer; } .controls-row { display: flex; gap: 15px; margin-top: 15px; } </style> </head> <body> <!-- Overlay for initial instructions --> <div id="ui-overlay" class="ui-container"> <h1>Webcam Voxel Matrix</h1> <p>This app visualizes your webcam feed as a 3D grid of cubes. Click 'Start Webcam' and grant camera permission to begin.</p> <button id="start-webcam-btn" class="ui-button">Start Webcam</button> </div> <!-- Control panel for sliders --> <div id="control-panel" class="ui-container controls hidden"> <h2>Controls</h2> <div class="slider-group"> <label for="scale-amount-slider">Scale Amount</label> <input type="range" id="scale-amount-slider" min="0.1" max="5.0" step="0.1" value="1.5"> </div> <div class="slider-group"> <label for="z-offset-slider">Z-Offset</label> <input type="range" id="z-offset-slider" min="0.0" max="80.0" step="0.1" value="20.0"> </div> <div class="slider-group"> <label for="position-smoothing-slider">Position Smoothing</label> <input type="range" id="position-smoothing-slider" min="0.01" max="0.5" step="0.01" value="0.05"> </div> <div class="slider-group"> <label for="rotation-smoothing-slider">Rotation Smoothing</label> <input type="range" id="rotation-smoothing-slider" min="0.01" max="0.5" step="0.01" value="0.02"> </div> <div class="slider-group"> <label for="light-intensity-slider">Light Intensity</label> <input type="range" id="light-intensity-slider" min="0.1" max="2.0" step="0.1" value="1.0"> </div> <div class="slider-group"> <label for="light-direction-slider">Light Direction</label> <input type="range" id="light-direction-slider" min="0" max="360" step="1" value="45"> </div> <div class="controls-row"> <button id="toggle-bloom-btn" class="ui-button">Disable Bloom</button> <button id="fullscreen-btn" class="ui-button">Go Fullscreen</button> </div> <div id="bloom-controls"> <div class="slider-group"> <label for="bloom-strength-slider">Bloom Strength</label> <input type="range" id="bloom-strength-slider" min="0.0" max="3.0" step="0.1" value="1.0"> </div> <div class="slider-group"> <label for="bloom-threshold-slider">Bloom Threshold</label> <input type="range" id="bloom-threshold-slider" min="0.0" max="1.0" step="0.01" value="0.5"> </div> <div class="slider-group"> <label for="bloom-radius-slider">Bloom Radius</label> <input type="range" id="bloom-radius-slider" min="0.0" max="1.0" step="0.01" value="0.4"> </div> </div> <div id="fog-controls"> <div class="slider-group"> <label for="fog-near-slider">Fog Near</label> <input type="range" id="fog-near-slider" min="1" max="200" step="1" value="100"> </div> <div class="slider-group"> <label for="fog-far-slider">Fog Far</label> <input type="range" id="fog-far-slider" min="200" max="1000" step="1" value="300"> </div> </div> </div> <!-- Hidden video and canvas for webcam feed processing --> <video id="webcam-video" autoplay playsinline></video> <canvas id="webcam-canvas"></canvas> <!-- Three.js Library --> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <!-- OrbitControls for camera interaction --> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script> <!-- Post-processing shaders --> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script> <!-- Post-processing passes --> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script> <script> // Global variables for Three.js scene, camera, and renderer let scene, camera, renderer, controls, composer; let instancedMesh; let dummy = new THREE.Object3D(); // Webcam and canvas variables for video frame processing let video, webcamCanvas, webcamContext; // Post-processing passes let renderPass, unrealBloomPass; let isBloomEnabled = true; // Voxel grid resolution and number of instances const CUBE_COUNT_X = 400; const CUBE_COUNT_Y = 300; const NUM_INSTANCES = CUBE_COUNT_X * CUBE_COUNT_Y; const CUBE_SPACING = 1.0; // UI Control variables, initialized with default values let scaleAmountMultiplier = 1.5; let zOffsetMultiplier = 20.0; let positionSmoothingFactor = 0.05; let rotationSmoothingFactor = 0.02; let lightIntensity = 1.0; let lightDirectionAngle = 45; let fogNear = 100; let fogFar = 300; // Array to store smoothed data for each cube let smoothedData = []; for(let i = 0; i < NUM_INSTANCES; i++) { smoothedData.push({ scale: new THREE.Vector3(1, 1, 1), rotation: new THREE.Euler(0, 0, 0), zPos: 0 }); } // Lighting let ambientLight; let directionalLight1; // Function to create and position the cubes in the scene function createAndPositionCubes() { // Remove previous instanced mesh if it exists to prevent memory leaks if (instancedMesh) { scene.remove(instancedMesh); instancedMesh.geometry.dispose(); instancedMesh.material.dispose(); } // Create the cubes using InstancedMesh for high performance const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshLambertMaterial({ color: 0xffffff }); instancedMesh = new THREE.InstancedMesh(geometry, material, NUM_INSTANCES); scene.add(instancedMesh); const totalWidth = CUBE_COUNT_X * CUBE_SPACING; const totalHeight = CUBE_COUNT_Y * CUBE_SPACING; // Loop through the grid and set the initial position for each cube for (let y = 0; y < CUBE_COUNT_Y; y++) { for (let x = 0; x < CUBE_COUNT_X; x++) { const i = y * CUBE_COUNT_X + x; dummy.position.x = x * CUBE_SPACING - totalWidth / 2 + CUBE_SPACING / 2; dummy.position.y = (CUBE_COUNT_Y - 1 - y) * CUBE_SPACING - totalHeight / 2 + CUBE_SPACING / 2; dummy.position.z = 0; dummy.rotation.set(0, 0, 0); dummy.scale.set(1, 1, 1); dummy.updateMatrix(); instancedMesh.setMatrixAt(i, dummy.matrix); } } instancedMesh.instanceMatrix.needsUpdate = true; camera.position.z = Math.max(totalWidth, totalHeight) * 1.5; camera.updateProjectionMatrix(); } // Initialize the main Three.js scene and controls function initThreeJS() { // Scene scene = new THREE.Scene(); scene.fog = new THREE.Fog(0x111111, fogNear, fogFar); // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); // Renderer if (!renderer) { renderer = new THREE.WebGLRenderer({ antialias: true }); document.body.appendChild(renderer.domElement); } renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x111111, 1); // Controls if (!controls) { controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.25; controls.enableZoom = true; } // Lighting ambientLight = new THREE.AmbientLight(0xffffff, 0); // Ambient light set to black scene.add(ambientLight); directionalLight1 = new THREE.DirectionalLight(0xffffff, lightIntensity); directionalLight1.position.set(100, 100, 100); scene.add(directionalLight1); // Post-processing setup composer = new THREE.EffectComposer(renderer); renderPass = new THREE.RenderPass(scene, camera); composer.addPass(renderPass); // Initialize Bloom pass, starting with a default strength unrealBloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.0, 0.4, 0.85); composer.addPass(unrealBloomPass); // Create and position cubes initially createAndPositionCubes(); } // Initialize the webcam and start the video stream async function initWebcam() { video = document.getElementById('webcam-video'); webcamCanvas = document.getElementById('webcam-canvas'); webcamCanvas.width = CUBE_COUNT_X; webcamCanvas.height = CUBE_COUNT_Y; webcamContext = webcamCanvas.getContext('2d', { willReadFrequently: true }); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: CUBE_COUNT_X, height: CUBE_COUNT_Y } }); video.srcObject = stream; video.onloadedmetadata = () => { video.play(); // Hide the UI overlay and show the controls once the webcam is ready document.getElementById('ui-overlay').classList.add('hidden'); document.getElementById('control-panel').classList.remove('hidden'); animate(); // Start the animation loop }; } catch (err) { console.error("Error accessing the webcam: ", err); const overlay = document.getElementById('ui-overlay'); overlay.innerHTML = ` <h1>Error</h1> <p>Could not access webcam. Please ensure you have a camera connected and grant permission.</p> `; } } // The main animation loop for the scene function animate() { requestAnimationFrame(animate); // Update OrbitControls controls.update(); // Update directional light properties from sliders directionalLight1.intensity = lightIntensity; const angle1 = lightDirectionAngle * Math.PI / 180; directionalLight1.position.x = Math.sin(angle1) * 150; directionalLight1.position.z = Math.cos(angle1) * 150; // Update fog properties scene.fog.near = fogNear; scene.fog.far = fogFar; if (video && video.readyState === video.HAVE_ENOUGH_DATA) { // Draw a frame from the video to the hidden 2D canvas webcamContext.drawImage(video, 0, 0, CUBE_COUNT_X, CUBE_COUNT_Y); const imageData = webcamContext.getImageData(0, 0, CUBE_COUNT_X, CUBE_COUNT_Y).data; // Process each pixel and update the corresponding cube for (let y = 0; y < CUBE_COUNT_Y; y++) { for (let x = 0; x < CUBE_COUNT_X; x++) { const i = y * CUBE_COUNT_X + x; const pixelIndex = i * 4; const r = imageData[pixelIndex]; const g = imageData[pixelIndex + 1]; const b = imageData[pixelIndex + 2]; // Calculate brightness (average of RGB) const brightness = (r + g + b) / 3; // Map brightness to cube size and Z-position const normalizedBrightness = brightness / 255; const newScale = normalizedBrightness * scaleAmountMultiplier + 0.1; const newZPosition = (normalizedBrightness - 0.5) * zOffsetMultiplier; // Apply smoothing to the new values smoothedData[i].scale.x += (newScale - smoothedData[i].scale.x) * positionSmoothingFactor; smoothedData[i].scale.y += (newScale - smoothedData[i].scale.y) * positionSmoothingFactor; smoothedData[i].scale.z += (newScale - smoothedData[i].scale.z) * positionSmoothingFactor; smoothedData[i].zPos += (newZPosition - smoothedData[i].zPos) * positionSmoothingFactor; // Map color channels to rotation angles const newRotationX = (r / 255) * Math.PI * 2; const newRotationY = (g / 255) * Math.PI * 2; const newRotationZ = (b / 255) * Math.PI * 2; // Apply smoothing twice for a more gradual rotation change smoothedData[i].rotation.x += (newRotationX - smoothedData[i].rotation.x) * rotationSmoothingFactor; smoothedData[i].rotation.y += (newRotationY - smoothedData[i].rotation.y) * rotationSmoothingFactor; smoothedData[i].rotation.z += (newRotationZ - smoothedData[i].rotation.z) * rotationSmoothingFactor; // Update the dummy object with new values dummy.position.x = x * CUBE_SPACING - (CUBE_COUNT_X * CUBE_SPACING) / 2 + CUBE_SPACING / 2; dummy.position.y = (CUBE_COUNT_Y - 1 - y) * CUBE_SPACING - (CUBE_COUNT_Y * CUBE_SPACING) / 2 + CUBE_SPACING / 2; dummy.position.z = smoothedData[i].zPos; dummy.scale.copy(smoothedData[i].scale); dummy.rotation.set( smoothedData[i].rotation.x + (newRotationX - smoothedData[i].rotation.x) * rotationSmoothingFactor, smoothedData[i].rotation.y + (newRotationY - smoothedData[i].rotation.y) * rotationSmoothingFactor, smoothedData[i].rotation.z + (newRotationZ - smoothedData[i].rotation.z) * rotationSmoothingFactor ); dummy.updateMatrix(); instancedMesh.setMatrixAt(i, dummy.matrix); } } // Tell Three.js to update the instanced mesh with the new matrices instancedMesh.instanceMatrix.needsUpdate = true; } // Render the scene using the composer for post-processing effects composer.render(); } // Handle window resizing window.addEventListener('resize', () => { if (camera && renderer && composer) { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); } }, false); // Event listener for the "Start Webcam" button document.getElementById('start-webcam-btn').addEventListener('click', () => { initThreeJS(); initWebcam(); }); // Event listeners for the UI sliders document.getElementById('scale-amount-slider').addEventListener('input', (e) => { scaleAmountMultiplier = parseFloat(e.target.value); }); document.getElementById('z-offset-slider').addEventListener('input', (e) => { zOffsetMultiplier = parseFloat(e.target.value); }); document.getElementById('position-smoothing-slider').addEventListener('input', (e) => { positionSmoothingFactor = parseFloat(e.target.value); }); document.getElementById('rotation-smoothing-slider').addEventListener('input', (e) => { rotationSmoothingFactor = parseFloat(e.target.value); }); document.getElementById('light-intensity-slider').addEventListener('input', (e) => { lightIntensity = parseFloat(e.target.value); }); document.getElementById('light-direction-slider').addEventListener('input', (e) => { lightDirectionAngle = parseFloat(e.target.value); }); document.getElementById('fog-near-slider').addEventListener('input', (e) => { fogNear = parseFloat(e.target.value); }); document.getElementById('fog-far-slider').addEventListener('input', (e) => { fogFar = parseFloat(e.target.value); }); // Toggle button for bloom effect document.getElementById('toggle-bloom-btn').addEventListener('click', () => { isBloomEnabled = !isBloomEnabled; const button = document.getElementById('toggle-bloom-btn'); if (isBloomEnabled) { unrealBloomPass.strength = parseFloat(document.getElementById('bloom-strength-slider').value); button.textContent = 'Disable Bloom'; } else { unrealBloomPass.strength = 0; button.textContent = 'Enable Bloom'; } }); // Event listeners for Bloom sliders document.getElementById('bloom-strength-slider').addEventListener('input', (e) => { if (isBloomEnabled) { unrealBloomPass.strength = parseFloat(e.target.value); } }); document.getElementById('bloom-threshold-slider').addEventListener('input', (e) => { if (isBloomEnabled) { unrealBloomPass.threshold = parseFloat(e.target.value); } }); document.getElementById('bloom-radius-slider').addEventListener('input', (e) => { if (isBloomEnabled) { unrealBloomPass.radius = parseFloat(e.target.value); } }); // Fullscreen button event listener document.getElementById('fullscreen-btn').addEventListener('click', () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().then(() => { document.getElementById('fullscreen-btn').textContent = 'Exit Fullscreen'; }).catch(err => { console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`); }); } else { document.exitFullscreen().then(() => { document.getElementById('fullscreen-btn').textContent = 'Go Fullscreen'; }); } }); </script> </body> </html>