/** * 3D DRUM VISUALIZER WITH WEBGL * Advanced graphics engine for immersive drum learning * * Features: * - 3D drum kit with realistic physics * - Particle systems for each hit * - Audio-reactive visualizations * - Dynamic lighting and bloom effects * - Performance optimized for 60fps */ class DrumVisualizer3D { constructor(canvasId) { this.canvas = document.getElementById(canvasId); if (!this.canvas) { console.error('Canvas not found:', canvasId); return; } this.gl = this.canvas.getContext('webgl2', { alpha: true, antialias: true, preserveDrawingBuffer: false }); if (!this.gl) { console.error('WebGL 2 not supported'); return; } // Scene setup this.camera = { position: [0, 2, 5], rotation: [0, 0, 0], fov: 45, near: 0.1, far: 100 }; this.lights = []; this.particles = []; this.drumMeshes = new Map(); this.animations = []; // Performance tracking this.fps = 60; this.lastFrame = 0; this.frameCount = 0; this.init(); console.log('🎨 3D Visualizer initialized'); } /** * Initialize WebGL scene */ init() { const gl = this.gl; // Setup viewport this.resize(); window.addEventListener('resize', () => this.resize()); // Enable depth testing gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); // Enable blending for particles gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Clear color gl.clearColor(0.02, 0.02, 0.05, 1.0); // Initialize shaders this.initShaders(); // Create 3D drum kit this.createDrumKit(); // Setup lighting this.setupLighting(); // Start render loop this.render(); } /** * Initialize shader programs */ initShaders() { const gl = this.gl; // Vertex shader for drum meshes const drumVertexShader = `#version 300 es precision highp float; in vec3 aPosition; in vec3 aNormal; in vec2 aTexCoord; uniform mat4 uModelMatrix; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; uniform mat3 uNormalMatrix; out vec3 vNormal; out vec3 vPosition; out vec2 vTexCoord; void main() { vec4 worldPosition = uModelMatrix * vec4(aPosition, 1.0); vPosition = worldPosition.xyz; vNormal = normalize(uNormalMatrix * aNormal); vTexCoord = aTexCoord; gl_Position = uProjectionMatrix * uViewMatrix * worldPosition; } `; // Fragment shader with Phong lighting const drumFragmentShader = `#version 300 es precision highp float; in vec3 vNormal; in vec3 vPosition; in vec2 vTexCoord; uniform vec3 uLightPosition; uniform vec3 uCameraPosition; uniform vec3 uDrumColor; uniform float uGlow; uniform float uMetallic; out vec4 fragColor; void main() { // Lighting calculations vec3 normal = normalize(vNormal); vec3 lightDir = normalize(uLightPosition - vPosition); vec3 viewDir = normalize(uCameraPosition - vPosition); vec3 reflectDir = reflect(-lightDir, normal); // Diffuse float diff = max(dot(normal, lightDir), 0.0); // Specular float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); // Rim lighting for depth float rim = 1.0 - max(dot(viewDir, normal), 0.0); rim = pow(rim, 3.0); // Combine vec3 ambient = uDrumColor * 0.3; vec3 diffuse = uDrumColor * diff * 0.6; vec3 specular = vec3(1.0) * spec * uMetallic; vec3 rimLight = vec3(0.0, 1.0, 1.0) * rim * 0.5; vec3 color = ambient + diffuse + specular + rimLight; // Add glow effect when hit color += uDrumColor * uGlow * 2.0; fragColor = vec4(color, 1.0); } `; // Particle shader const particleVertexShader = `#version 300 es precision highp float; in vec3 aPosition; in float aSize; in vec4 aColor; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; out vec4 vColor; void main() { vColor = aColor; gl_Position = uProjectionMatrix * uViewMatrix * vec4(aPosition, 1.0); gl_PointSize = aSize; } `; const particleFragmentShader = `#version 300 es precision highp float; in vec4 vColor; out vec4 fragColor; void main() { // Circular particle vec2 coord = gl_PointCoord - vec2(0.5); float dist = length(coord); if (dist > 0.5) discard; float alpha = vColor.a * (1.0 - dist * 2.0); fragColor = vec4(vColor.rgb, alpha); } `; // Compile shaders this.drumProgram = this.createProgram(drumVertexShader, drumFragmentShader); this.particleProgram = this.createProgram(particleVertexShader, particleFragmentShader); } /** * Create shader program */ createProgram(vertexSource, fragmentSource) { const gl = this.gl; const vertexShader = this.compileShader(gl.VERTEX_SHADER, vertexSource); const fragmentShader = this.compileShader(gl.FRAGMENT_SHADER, fragmentSource); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('Program link error:', gl.getProgramInfoLog(program)); return null; } return program; } /** * Compile shader */ compileShader(type, source) { const gl = this.gl; const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('Shader compile error:', gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } /** * Create 3D drum kit geometry */ createDrumKit() { // Drum positions (X, Y, Z) const drumPositions = { 'Kick': [0, -0.5, 0], 'Snare': [-0.5, 0, 0.5], 'Hi-Hat': [-1, 0.3, 0.8], 'Tom 1': [0.3, 0.2, 0.3], 'Tom 2': [0.7, 0.1, 0.2], 'Tom 3': [0.9, -0.1, -0.3], 'Crash': [-1.2, 0.8, 0.5], 'Ride': [1.2, 0.7, 0] }; // Drum colors (RGB) const drumColors = { 'Kick': [0.8, 0.2, 0.2], // Red 'Snare': [0.2, 0.8, 1.0], // Cyan 'Hi-Hat': [1.0, 1.0, 0.2], // Yellow 'Tom 1': [1.0, 0.5, 0.0], // Orange 'Tom 2': [0.8, 0.3, 1.0], // Purple 'Tom 3': [0.3, 1.0, 0.3], // Green 'Crash': [1.0, 0.2, 1.0], // Magenta 'Ride': [0.2, 1.0, 0.8] // Teal }; // Create mesh for each drum Object.entries(drumPositions).forEach(([name, position]) => { const isCymbal = name.includes('Crash') || name.includes('Ride') || name.includes('Hi-Hat'); const geometry = isCymbal ? this.createCylinderGeometry(0.4, 0.02, 32) : this.createCylinderGeometry(0.3, 0.4, 32); this.drumMeshes.set(name, { name, geometry, position, color: drumColors[name], rotation: [0, 0, 0], scale: [1, 1, 1], glow: 0, metallic: isCymbal ? 0.9 : 0.3 }); }); } /** * Create cylinder geometry (for drums and cymbals) */ createCylinderGeometry(radius, height, segments) { const positions = []; const normals = []; const indices = []; // Top and bottom caps for (let y = 0; y <= 1; y++) { const yPos = y === 0 ? -height / 2 : height / 2; for (let i = 0; i <= segments; i++) { const angle = (i / segments) * Math.PI * 2; const x = Math.cos(angle) * radius; const z = Math.sin(angle) * radius; positions.push(x, yPos, z); normals.push(0, y === 0 ? -1 : 1, 0); } } // Side for (let i = 0; i <= segments; i++) { const angle = (i / segments) * Math.PI * 2; const x = Math.cos(angle) * radius; const z = Math.sin(angle) * radius; positions.push(x, -height / 2, z); normals.push(Math.cos(angle), 0, Math.sin(angle)); positions.push(x, height / 2, z); normals.push(Math.cos(angle), 0, Math.sin(angle)); } // Create indices const capStart1 = 0; const capStart2 = segments + 1; const sideStart = (segments + 1) * 2; // Top and bottom cap indices for (let i = 1; i < segments; i++) { indices.push(capStart1, capStart1 + i, capStart1 + i + 1); indices.push(capStart2, capStart2 + i + 1, capStart2 + i); } // Side indices for (let i = 0; i < segments; i++) { const base = sideStart + i * 2; indices.push(base, base + 1, base + 2); indices.push(base + 1, base + 3, base + 2); } return { positions: new Float32Array(positions), normals: new Float32Array(normals), indices: new Uint16Array(indices) }; } /** * Setup dynamic lighting */ setupLighting() { this.lights = [ { position: [2, 4, 3], color: [1.0, 1.0, 1.0], intensity: 1.0 }, { position: [-2, 3, 2], color: [0.0, 1.0, 1.0], intensity: 0.5 }, { position: [0, 5, -2], color: [1.0, 0.0, 1.0], intensity: 0.3 } ]; } /** * Trigger drum hit animation */ hitDrum(drumName, velocity = 80) { const drum = this.drumMeshes.get(drumName); if (!drum) return; // Add glow effect drum.glow = velocity / 127; // Animate glow decay this.animations.push({ target: drum, property: 'glow', start: drum.glow, end: 0, duration: 500, startTime: Date.now() }); // Create particles this.createParticles(drum.position, drum.color, velocity); // Add slight bounce animation this.animations.push({ target: drum, property: 'position', axis: 1, // Y axis start: drum.position[1], end: drum.position[1] - 0.05, duration: 100, startTime: Date.now(), bounce: true }); } /** * Create particle explosion for drum hit */ createParticles(position, color, velocity) { const particleCount = Math.floor(velocity / 4) + 10; for (let i = 0; i < particleCount; i++) { const angle = Math.random() * Math.PI * 2; const speed = (Math.random() * 0.02 + 0.01) * (velocity / 127); const spread = Math.random() * 0.5 + 0.5; this.particles.push({ position: [...position], velocity: [ Math.cos(angle) * speed * spread, Math.random() * speed * 2, Math.sin(angle) * speed * spread ], color: [...color, 1.0], size: Math.random() * 10 + 5, life: 1.0, decay: 0.02 }); } } /** * Update animations */ updateAnimations(deltaTime) { const now = Date.now(); this.animations = this.animations.filter(anim => { const elapsed = now - anim.startTime; const progress = Math.min(elapsed / anim.duration, 1); if (anim.property === 'glow') { anim.target[anim.property] = anim.start + (anim.end - anim.start) * progress; } else if (anim.property === 'position') { const value = anim.start + (anim.end - anim.start) * progress; anim.target[anim.property][anim.axis] = value; // Bounce back if (progress >= 1 && anim.bounce && !anim.bounced) { anim.bounced = true; anim.startTime = now; [anim.start, anim.end] = [anim.end, anim.start]; return true; } } return progress < 1 || (anim.bounce && !anim.bounced); }); // Update particles this.particles = this.particles.filter(particle => { particle.position[0] += particle.velocity[0]; particle.position[1] += particle.velocity[1]; particle.position[2] += particle.velocity[2]; particle.velocity[1] -= 0.001; // Gravity particle.life -= particle.decay; particle.color[3] = particle.life; return particle.life > 0; }); } /** * Render scene */ render(timestamp = 0) { const deltaTime = timestamp - this.lastFrame; this.lastFrame = timestamp; requestAnimationFrame(t => this.render(t)); const gl = this.gl; // Update animations this.updateAnimations(deltaTime); // Clear gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Update camera (smooth rotation) const time = timestamp * 0.0001; this.camera.position[0] = Math.sin(time) * 5; this.camera.position[2] = Math.cos(time) * 5; // Calculate matrices const viewMatrix = this.createViewMatrix(); const projectionMatrix = this.createProjectionMatrix(); // Render drums this.renderDrums(viewMatrix, projectionMatrix); // Render particles this.renderParticles(viewMatrix, projectionMatrix); // Update FPS this.frameCount++; if (this.frameCount % 60 === 0) { this.fps = Math.round(1000 / deltaTime); } } /** * Render drum meshes */ renderDrums(viewMatrix, projectionMatrix) { const gl = this.gl; gl.useProgram(this.drumProgram); // Get uniform locations const uModelMatrix = gl.getUniformLocation(this.drumProgram, 'uModelMatrix'); const uViewMatrix = gl.getUniformLocation(this.drumProgram, 'uViewMatrix'); const uProjectionMatrix = gl.getUniformLocation(this.drumProgram, 'uProjectionMatrix'); const uNormalMatrix = gl.getUniformLocation(this.drumProgram, 'uNormalMatrix'); const uLightPosition = gl.getUniformLocation(this.drumProgram, 'uLightPosition'); const uCameraPosition = gl.getUniformLocation(this.drumProgram, 'uCameraPosition'); const uDrumColor = gl.getUniformLocation(this.drumProgram, 'uDrumColor'); const uGlow = gl.getUniformLocation(this.drumProgram, 'uGlow'); const uMetallic = gl.getUniformLocation(this.drumProgram, 'uMetallic'); // Set uniforms gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix); gl.uniformMatrix4fv(uProjectionMatrix, false, projectionMatrix); gl.uniform3fv(uLightPosition, this.lights[0].position); gl.uniform3fv(uCameraPosition, this.camera.position); // Render each drum this.drumMeshes.forEach(drum => { const modelMatrix = this.createModelMatrix(drum); const normalMatrix = this.createNormalMatrix(modelMatrix); gl.uniformMatrix4fv(uModelMatrix, false, modelMatrix); gl.uniformMatrix3fv(uNormalMatrix, false, normalMatrix); gl.uniform3fv(uDrumColor, drum.color); gl.uniform1f(uGlow, drum.glow); gl.uniform1f(uMetallic, drum.metallic); this.renderMesh(drum.geometry); }); } /** * Render particles */ renderParticles(viewMatrix, projectionMatrix) { if (this.particles.length === 0) return; const gl = this.gl; gl.useProgram(this.particleProgram); const positions = []; const sizes = []; const colors = []; this.particles.forEach(particle => { positions.push(...particle.position); sizes.push(particle.size); colors.push(...particle.color); }); // Create buffers const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW); const aPosition = gl.getAttribLocation(this.particleProgram, 'aPosition'); gl.enableVertexAttribArray(aPosition); gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0); // Set uniforms const uViewMatrix = gl.getUniformLocation(this.particleProgram, 'uViewMatrix'); const uProjectionMatrix = gl.getUniformLocation(this.particleProgram, 'uProjectionMatrix'); gl.uniformMatrix4fv(uViewMatrix, false, viewMatrix); gl.uniformMatrix4fv(uProjectionMatrix, false, projectionMatrix); // Draw particles gl.drawArrays(gl.POINTS, 0, this.particles.length); gl.deleteBuffer(positionBuffer); } /** * Helper: Render mesh */ renderMesh(geometry) { const gl = this.gl; // Position buffer const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, geometry.positions, gl.STATIC_DRAW); const aPosition = gl.getAttribLocation(this.drumProgram, 'aPosition'); gl.enableVertexAttribArray(aPosition); gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0); // Normal buffer const normalBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); gl.bufferData(gl.ARRAY_BUFFER, geometry.normals, gl.STATIC_DRAW); const aNormal = gl.getAttribLocation(this.drumProgram, 'aNormal'); gl.enableVertexAttribArray(aNormal); gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0); // Index buffer const indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.indices, gl.STATIC_DRAW); // Draw gl.drawElements(gl.TRIANGLES, geometry.indices.length, gl.UNSIGNED_SHORT, 0); // Cleanup gl.deleteBuffer(positionBuffer); gl.deleteBuffer(normalBuffer); gl.deleteBuffer(indexBuffer); } /** * Matrix operations */ createModelMatrix(drum) { const matrix = this.createIdentityMatrix(); this.translate(matrix, drum.position); this.scale(matrix, drum.scale); return matrix; } createViewMatrix() { const matrix = this.createIdentityMatrix(); this.translate(matrix, this.camera.position.map(v => -v)); return matrix; } createProjectionMatrix() { const aspect = this.canvas.width / this.canvas.height; return this.perspective(this.camera.fov, aspect, this.camera.near, this.camera.far); } createNormalMatrix(modelMatrix) { // Simplified 3x3 normal matrix from 4x4 model matrix return new Float32Array([ modelMatrix[0], modelMatrix[1], modelMatrix[2], modelMatrix[4], modelMatrix[5], modelMatrix[6], modelMatrix[8], modelMatrix[9], modelMatrix[10] ]); } createIdentityMatrix() { return new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); } translate(matrix, vector) { matrix[12] += vector[0]; matrix[13] += vector[1]; matrix[14] += vector[2]; } scale(matrix, vector) { matrix[0] *= vector[0]; matrix[5] *= vector[1]; matrix[10] *= vector[2]; } perspective(fov, aspect, near, far) { const f = 1.0 / Math.tan(fov * Math.PI / 360); const rangeInv = 1.0 / (near - far); return new Float32Array([ f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (near + far) * rangeInv, -1, 0, 0, near * far * rangeInv * 2, 0 ]); } resize() { this.canvas.width = this.canvas.clientWidth * window.devicePixelRatio; this.canvas.height = this.canvas.clientHeight * window.devicePixelRatio; this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); } /** * Get current FPS */ getFPS() { return this.fps; } /** * Cleanup */ destroy() { // Cancel animation frame this.gl.getExtension('WEBGL_lose_context').loseContext(); } } // Export for use if (typeof module !== 'undefined' && module.exports) { module.exports = DrumVisualizer3D; }