오디오 반응형 비주얼

Audio-reactive visual

음악의 주파수 대역별 세기(스펙트럼)에 맞춰 막대나 오브젝트의 크기를 실시간으로 흔드는 시각화 기법.

다른 이름: AnalyserNode spectrumMusic visualizerFFT bars
···
js
import * as THREE from 'three';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05060c);
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.set(0, 2.6, 5.4);
camera.lookAt(0, 0, 0);
scene.add(new THREE.AmbientLight(0x445577, 1.3));
const dl = new THREE.DirectionalLight(0xffffff, 1.1);
dl.position.set(3, 5, 3);
scene.add(dl);

const base = new THREE.Mesh(new THREE.PlaneGeometry(12, 4), new THREE.MeshBasicMaterial({ color: 0x0a0e1a }));
base.rotation.x = -Math.PI / 2;
base.position.y = -0.02;
scene.add(base);

const BARS = 28;
const geo = new THREE.BoxGeometry(0.28, 1, 0.28);
const mat = new THREE.MeshStandardMaterial({ roughness: 0.4 });
const mesh = new THREE.InstancedMesh(geo, mat, BARS);
const dummy = new THREE.Object3D();
const color = new THREE.Color();
const simplex = new SimplexNoise();
scene.add(mesh);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const time = t * 0.001;
  for (let i = 0; i < BARS; i++) {
    const band = i / (BARS - 1);
    const bass = Math.max(0, Math.sin(time * 2.4) * 0.5 + 0.5) * Math.exp(-band * 2.5);
    const n = (simplex.noise(band * 3.0, time * 1.6) + 1) * 0.5;
    const mag = 0.15 + bass * 1.1 + n * 0.9 * (1.0 - band * 0.4);
    dummy.position.set((i - (BARS - 1) / 2) * 0.36, mag / 2, 0);
    dummy.scale.set(1, Math.max(0.05, mag), 1);
    dummy.updateMatrix();
    mesh.setMatrixAt(i, dummy.matrix);
    color.setHSL(0.62 - band * 0.45, 0.85, 0.55 + mag * 0.12);
    mesh.setColorAt(i, color);
  }
  mesh.instanceMatrix.needsUpdate = true;
  if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
  renderer.render(scene, camera);
});

실제 서비스에서는 Web Audio API의 AnalyserNode.getByteFrequencyData()가 오디오를 주파수 대역별 세기 배열(FFT, 고속 푸리에 변환 결과)로 돌려주고, 그 배열의 각 칸을 막대 하나의 높이에 그대로 매핑합니다 — 낮은 인덱스가 저음(베이스), 높은 인덱스가 고음입니다. 이 사이트는 마이크·오디오 파일을 쓸 수 없으므로, 그 배열 대신 사인파 여러 개와 SimplexNoise를 섞어 "그럴듯한 스펙트럼"을 흉내 냅니다: 저음 대역은 느리고 강한 sin() 펄스로 쿵쿵 울리게, 나머지 대역은 노이즈로 자잘하게 흔들리게 만들고, 대역이 높아질수록 전체 크기를 지수적으로 줄여(Math.exp(-band*2.5)) 실제 음악 스펙트럼처럼 저음이 크고 고음이 작은 모양을 만듭니다.

막대 28개를 THREE.InstancedMesh 하나로 그려서(instanced-mesh 항목 참고) 드로우콜은 하나뿐입니다. 매 프레임 각 인스턴스의 변환 행렬(dummy.scale.y로 높이 조절)과 색(mesh.setColorAt)을 다시 계산해 mesh.instanceMatrix.needsUpdate = true / mesh.instanceColor.needsUpdate = true로 GPU에 반영합니다.

실제 오디오를 연결하려면 이 데모의 "가짜 스펙트럼 계산" 부분만 analyser.getByteFrequencyData(dataArray) 결과로 바꿔 끼우면 됩니다 — 렌더링 파이프라인은 그대로 재사용할 수 있습니다. 단, getByteFrequencyData는 사용자 제스처(클릭 등) 이후에만 오디오 컨텍스트가 재생 가능한 브라우저 정책을 감안해야 합니다.

언제 쓰나

음악 플레이어의 스펙트럼 이퀄라이저, 라이브 스트리밍 방송 화면, 음성 인식 대기 상태 표시에.