Skip to content

Instantly share code, notes, and snippets.

@iamnolanhu
Created June 19, 2025 09:38
Show Gist options
  • Save iamnolanhu/49227b4dd38a484b573ad018c23c4687 to your computer and use it in GitHub Desktop.
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…
<!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