Created
June 19, 2025 09:38
-
-
Save iamnolanhu/49227b4dd38a484b573ad018c23c4687 to your computer and use it in GitHub Desktop.
3D Audio Reactive Planet Visualizer - A WebGL music visualization featuring a rotating planet that responds to audio input with dynamic surface deformation and hypnotic color-shifting shaders. Built with Three.js, it creates an immersive experience with 5000 particle starfield, real-time frequency analysis, and smooth camera movements. Simply up…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>3D Planet Music Visualizer</title> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
background: #000; | |
font-family: Arial, sans-serif; | |
} | |
#canvas { | |
display: block; | |
} | |
#controls { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
z-index: 100; | |
background: rgba(0, 0, 0, 0.7); | |
padding: 20px; | |
border-radius: 10px; | |
color: white; | |
} | |
#fileInput { | |
display: block; | |
margin-bottom: 10px; | |
padding: 10px; | |
background: #333; | |
color: white; | |
border: 1px solid #555; | |
border-radius: 5px; | |
cursor: pointer; | |
} | |
#info { | |
margin-top: 10px; | |
font-size: 14px; | |
color: #aaa; | |
} | |
button { | |
padding: 10px 20px; | |
margin: 5px; | |
background: #4CAF50; | |
color: white; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
font-size: 16px; | |
} | |
button:hover { | |
background: #45a049; | |
} | |
#colorShift { | |
margin-top: 10px; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas id="canvas"></canvas> | |
<div id="controls"> | |
<h3>3D Planet Music Visualizer</h3> | |
<input type="file" id="fileInput" accept="audio/*" /> | |
<button id="playPause" style="display: none;">Play</button> | |
<label id="colorShift"> | |
<input type="checkbox" id="colorShiftCheckbox" checked> | |
Color-shifting shaders | |
</label> | |
<div id="info">Upload an MP3 file to begin</div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script> | |
// Scene setup | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
camera.position.z = 5; | |
// Lighting | |
const ambientLight = new THREE.AmbientLight(0x404040); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); | |
directionalLight.position.set(5, 5, 5); | |
scene.add(directionalLight); | |
// Create planet with custom shader material | |
const planetGeometry = new THREE.SphereGeometry(2, 64, 64); | |
// Custom shader for hypnotic visual effect | |
const vertexShader = ` | |
varying vec2 vUv; | |
varying vec3 vNormal; | |
uniform float audioData[32]; | |
uniform float time; | |
void main() { | |
vUv = uv; | |
vNormal = normal; | |
vec3 pos = position; | |
float displacement = 0.0; | |
// Create wave effect based on audio | |
for(int i = 0; i < 32; i++) { | |
float freq = float(i) * 0.1; | |
displacement += audioData[i] * 0.01 * sin(pos.x * freq + time) * sin(pos.y * freq + time); | |
} | |
pos += normal * displacement; | |
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); | |
} | |
`; | |
const fragmentShader = ` | |
varying vec2 vUv; | |
varying vec3 vNormal; | |
uniform float audioData[32]; | |
uniform float time; | |
uniform bool colorShift; | |
void main() { | |
vec3 color; | |
if(colorShift) { | |
// Hypnotic color shifting based on audio and time | |
float r = 0.5 + 0.5 * sin(time + vUv.x * 10.0 + audioData[0] * 0.1); | |
float g = 0.5 + 0.5 * sin(time * 1.3 + vUv.y * 10.0 + audioData[16] * 0.1); | |
float b = 0.5 + 0.5 * sin(time * 1.7 + (vUv.x + vUv.y) * 10.0 + audioData[31] * 0.1); | |
color = vec3(r, g, b); | |
} else { | |
// Static planet colors | |
color = vec3(0.2, 0.5, 0.8); | |
} | |
// Add lighting effect | |
float lighting = dot(vNormal, vec3(0.5, 0.5, 0.5)) * 0.5 + 0.5; | |
color *= lighting; | |
gl_FragColor = vec4(color, 1.0); | |
} | |
`; | |
const audioDataArray = new Float32Array(32); | |
const planetMaterial = new THREE.ShaderMaterial({ | |
vertexShader, | |
fragmentShader, | |
uniforms: { | |
audioData: { value: audioDataArray }, | |
time: { value: 0 }, | |
colorShift: { value: true } | |
} | |
}); | |
const planet = new THREE.Mesh(planetGeometry, planetMaterial); | |
scene.add(planet); | |
// Particle system for background | |
const particlesGeometry = new THREE.BufferGeometry(); | |
const particlesCount = 5000; | |
const posArray = new Float32Array(particlesCount * 3); | |
for(let i = 0; i < particlesCount * 3; i++) { | |
posArray[i] = (Math.random() - 0.5) * 50; | |
} | |
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); | |
const particlesMaterial = new THREE.PointsMaterial({ | |
size: 0.005, | |
color: 0xffffff | |
}); | |
const particlesMesh = new THREE.Points(particlesGeometry, particlesMaterial); | |
scene.add(particlesMesh); | |
// Audio setup | |
let audioContext, analyser, dataArray, source, audioBuffer; | |
let isPlaying = false; | |
const fileInput = document.getElementById('fileInput'); | |
const playPauseBtn = document.getElementById('playPause'); | |
const info = document.getElementById('info'); | |
const colorShiftCheckbox = document.getElementById('colorShiftCheckbox'); | |
fileInput.addEventListener('change', handleFileSelect); | |
playPauseBtn.addEventListener('click', togglePlayPause); | |
colorShiftCheckbox.addEventListener('change', (e) => { | |
planetMaterial.uniforms.colorShift.value = e.target.checked; | |
}); | |
function handleFileSelect(e) { | |
const file = e.target.files[0]; | |
if (!file) return; | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
setupAudio(e.target.result); | |
info.textContent = `Loaded: ${file.name}`; | |
playPauseBtn.style.display = 'inline-block'; | |
}; | |
reader.readAsArrayBuffer(file); | |
} | |
function setupAudio(arrayBuffer) { | |
if (!audioContext) { | |
audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
} | |
audioContext.decodeAudioData(arrayBuffer, (buffer) => { | |
audioBuffer = buffer; | |
analyser = audioContext.createAnalyser(); | |
analyser.fftSize = 64; | |
dataArray = new Uint8Array(analyser.frequencyBinCount); | |
}); | |
} | |
function togglePlayPause() { | |
if (!audioBuffer) return; | |
if (isPlaying) { | |
source.stop(); | |
isPlaying = false; | |
playPauseBtn.textContent = 'Play'; | |
} else { | |
source = audioContext.createBufferSource(); | |
source.buffer = audioBuffer; | |
source.connect(analyser); | |
analyser.connect(audioContext.destination); | |
source.start(0); | |
isPlaying = true; | |
playPauseBtn.textContent = 'Pause'; | |
source.onended = () => { | |
isPlaying = false; | |
playPauseBtn.textContent = 'Play'; | |
}; | |
} | |
} | |
// Animation loop | |
function animate() { | |
requestAnimationFrame(animate); | |
// Update audio data | |
if (analyser && isPlaying) { | |
analyser.getByteFrequencyData(dataArray); | |
// Normalize and smooth audio data | |
for(let i = 0; i < 32; i++) { | |
const value = dataArray[i] / 255; | |
audioDataArray[i] = audioDataArray[i] * 0.9 + value * 0.1; | |
} | |
planetMaterial.uniforms.audioData.value = audioDataArray; | |
} | |
// Update time uniform | |
planetMaterial.uniforms.time.value += 0.01; | |
// Rotate planet | |
planet.rotation.y += 0.005; | |
// Make particles float | |
particlesMesh.rotation.y += 0.0001; | |
// Subtle camera movement | |
camera.position.x = Math.sin(Date.now() * 0.0001) * 0.5; | |
camera.position.y = Math.cos(Date.now() * 0.0001) * 0.5; | |
camera.lookAt(planet.position); | |
renderer.render(scene, camera); | |
} | |
// Handle window resize | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
animate(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment