音乐可视化
今天读到了一篇关于 Three.js + Shader + 音乐可视化 的文章,介绍了如何利用 辉光特效(Bloom) 和 噪声函数(Noise) 来让一个球体随音乐节奏跳动。整体代码逻辑并不复杂,核心在于 Shader 的编写,尤其是 noise
函数的应用。
阅读后收获颇多,于是自己尝试替换了其中的 noise
函数,效果仍然不错。为了方便大家理解,我对原代码进行了简化,去除了非核心部分,只保留了主要实现逻辑。
无论是阅读原文还是我的简化版文章,应该都可以学到一些东西。
效果如下
一、整体功能概述
- 使用 WebGLRenderer 创建渲染器并配置抗锯齿效果。
- 创建 场景、相机、轨道控制器 以及 后期处理通道(辉光特效 + 合成输出)。
- 使用 自定义着色器(ShaderMaterial),并通过加载外部
.glsl
文件来编写顶点、片元着色器。 - 利用 Audio、AudioAnalyser 等 Three.js 内置音频模块,实现音频可视化,将音频频率数据传递给着色器进行动画变形。
- 为了在用户交互(点击)后才播放音频,示例中添加了对
document
的点击事件监听。
二、项目结构和主要依赖
1. 依赖安装
- Three.js:核心库(需要导入三维场景、相机、渲染器、后期处理、音频模块等)。
- OrbitControls:辅助控制器,用来在场景中交互旋转、缩放、移动相机视角。
- EffectComposer、RenderPass、UnrealBloomPass、OutputPass:Three.js 提供的后期处理模块,用于在场景渲染结果上叠加特效(这里主要演示发光效果 Bloom)。
- FileLoader、AudioLoader:Three.js 中用来加载外部文件的类。
2. 目录组织示例
project
│
├── src
│ ├── main.js // 这里放置本文示例中的主要逻辑代码
│ ├── shaders
│ │ ├── vertex.glsl // 顶点着色器
│ │ └── fragment.glsl // 片元着色器
│ └── Beats.mp3 // 音频资源
│
└── package.json
三、核心代码解析
1. 初始化 Three.js 渲染器、场景、相机
// 创建WebGL渲染器,启用抗锯齿效果
const renderer = new THREE.WebGLRenderer({ antialias: true });
// 设置渲染器尺寸为窗口大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 将渲染器的DOM元素添加到页面body中
document.body.appendChild(renderer.domElement);
// 创建场景对象,用于存放所有3D对象
const scene = new THREE.Scene();
// 创建透视相机,参数分别是:视场角度、宽高比、近裁剪面、远裁剪面
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
// 轨道控制器,用于鼠标交互
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
- renderer:使用
new THREE.WebGLRenderer
初始化,antialias: true
代表开启抗锯齿以减少锯齿现象。 - scene:一个场景容器,所有的 3D 模型、灯光等都会添加到其中。
- camera:使用透视投影的相机,设置
FOV(视场角)
为 45°。0.1
和1000
代表相机的近截面和远截面距离。 - OrbitControls:允许通过拖拽、滚轮等方式平移、旋转、缩放视图。
2. 后期处理:辉光通道、效果合成器
// 创建渲染通道,用于后期处理
const renderScene = new RenderPass(scene, camera);
// 创建辉光通道,为场景添加发光效果
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight));
// 设置辉光阈值,只有亮度超过此值的像素才会发光
bloomPass.threshold = 0.5;
// 设置辉光强度
bloomPass.strength = 0.5;
// 设置辉光半径
bloomPass.radius = 0.4;
// 创建效果合成器,用于组合多个后期处理效果
const bloomComposer = new EffectComposer(renderer);
// 添加渲染通道
bloomComposer.addPass(renderScene);
// 添加辉光通道
bloomComposer.addPass(bloomPass);
// 创建输出通道,用于最终渲染结果的输出
const outputPass = new OutputPass();
bloomComposer.addPass(outputPass);
- RenderPass:后期处理前,必须先把场景渲染到一个中间缓冲中。
- UnrealBloomPass:实现 Bloom 效果的通道,可通过
threshold
、strength
、radius
来调节发光效果。 - EffectComposer:用于组合多个后期处理 Pass,先渲染然后添加辉光,再把结果通过
OutputPass
输出到屏幕。
3. 相机设置
// 设置相机位置
camera.position.set(0, -2, 14);
// 设置相机朝向场景中心
camera.lookAt(0, 0, 0);
- 将相机在 Z 轴正方向往后移一些,并微调 Y 轴,得到一个合适的俯视/正视角度。
lookAt(0, 0, 0)
使相机视线指向场景中心。
4. 着色器加载与创建 ShaderMaterial
我们的自定义着色器分为 顶点着色器 和 片元着色器,源码放在外部 .glsl
文件中。
4.1 定义 uniforms
const uniforms = {
u_time: { type: "f", value: 0.0 }, // 时间变量
u_frequency: { type: "f", value: 0.0 }, // 音频频率
u_red: { type: "f", value: 0.3 }, // 颜色 R 分量
u_green: { type: "f", value: 1.0 }, // 颜色 G 分量
u_blue: { type: "f", value: 0.6 }, // 颜色 B 分量
};
u_time
:让物体或波动产生动画。u_frequency
:用来从音频分析器获得的频率值,驱动几何体变形。u_red / u_green / u_blue
:可用来在片元着色器中控制输出颜色。
4.2 加载着色器文件
function loadShader(path) {
return new Promise((resolve, reject) => {
const loader = new FileLoader();
loader.load(
path,
(data) => {
resolve(data);
},
undefined,
(err) => {
console.error(`加载着色器文件 ${path} 失败:`, err);
reject(err);
}
);
});
}
- 使用
FileLoader
异步加载.glsl
文件。 - 返回一个
Promise
,以便我们在后面可以通过await
同步获取结果。
4.3 创建并应用 ShaderMaterial
async function initShaders() {
try {
// 加载顶点和片元着色器
const vertexShader = await loadShader("/shaders/vertex.glsl");
const fragmentShader = await loadShader("/shaders/fragment.glsl");
// 创建着色器材质
const mat = new THREE.ShaderMaterial({
uniforms,
vertexShader,
fragmentShader,
});
// 创建二十面体几何体,用于雷达效果
const geo = new THREE.IcosahedronGeometry(4, 30);
// 创建网格并添加到场景
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
// 设置为线框模式
mesh.material.wireframe = true;
// 启动动画循环
animate();
} catch (error) {
console.error("初始化着色器失败:", error);
}
}
- 这里的
new THREE.IcosahedronGeometry(4, 30)
生成了一个半径为 4 的二十面体,并把细分等级设置比较高(30),使其在变形时更柔和、更细腻。 wireframe = true
:给网格一个线框模式,呈现科技感或科幻风格。animate()
:初始化完成后启动动画循环。
5. 着色器代码剖析
5.1 顶点着色器(vertex.glsl
)
这个顶点着色器的作用是:
- 创建一个随时间变化的动态表面
- 表面会产生波浪般的起伏效果
- 起伏的强度受频率参数控制
- 整体效果像是一个动态的雷达波或水面波纹
uniform float u_time;
uniform float u_frequency;
// 简化的噪声函数辅助方法
float random(vec2 st){
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}
// 使用分形叠加
// p 是三维空间中的一个点, rep 控制噪声的重复周期
float improvedNoise(vec3 p,vec3 rep){
// 创建基础噪声
float result=0.;
float amplitude=1.;
float frequency=1.;
float maxValue=0.;
// 分形布朗运动 - 叠加不同频率的噪声
for(int i=0;i<3;i++){
vec2 grid=floor(mod(p.xy*frequency,rep.xy));
vec2 f=fract(p.xy*frequency);
// 四个角的随机值
float a=random(grid);
float b=random(grid+vec2(1.,0.));
float c=random(grid+vec2(0.,1.));
float d=random(grid+vec2(1.,1.));
// 平滑插值 - 使用更平滑的曲线
vec2 u=f*f*f*(f*(f*6.-15.)+10.);// 更平滑的Perlin插值曲线
// 混合四个角的值
float noise=mix(
mix(a,b,u.x),
mix(c,d,u.x),
u.y
);
// 添加旋转变化,使噪声在z轴上更有变化
float zAngle=p.z*frequency/rep.z;
float zFactor=.5+.5*sin(zAngle*6.2831853);
// 累加噪声
result+=noise*amplitude*zFactor;
maxValue+=amplitude;
// 调整下一次迭代的频率和振幅
amplitude*=.5;
frequency*=2.;
}
// 归一化结果
return result/maxValue*2.2;
}
void main(){
// 计算噪声值,加入时间因素使其随时间变化
float noise = 3. * improvedNoise(position + vec3(u_time*.5, u_time*.3, u_time*.7), vec3(10.));
// 计算位移量,由频率和噪声共同决定
float displacement = (u_frequency / 30.) * (noise / 10.);
// 沿着顶点法线方向应用位移,创建起伏效果
vec3 newPosition = position + normal * displacement;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.);
}
position
、normal
:Three.js 自动注入,分别是顶点坐标和顶点法线。u_time
:时间用来推动噪声,产生持续变化的动画。u_frequency
:音频频率值决定顶点位移程度。improvedNoise
:模拟类似 Perlin 的噪声,带有分形叠加效果,能得到更复杂的波动形变。
5.2 片元着色器(fragment.glsl
)
uniform float u_red;
uniform float u_blue;
uniform float u_green;
void main(){
gl_FragColor = vec4(u_red, u_green, u_blue, 1.);
}
- 简单地把像素颜色设置成
u_red, u_green, u_blue
的组合值,可根据需求灵活调整。 - 如果想让颜色也随时间或频率变化,可以在这里加入更多逻辑或噪声计算。
6. 音频模块与可视化
// 创建音频监听器
const listener = new THREE.AudioListener();
camera.add(listener);
// 创建音频对象
const sound = new THREE.Audio(listener);
// 创建音频加载器
const audioLoader = new THREE.AudioLoader();
// 加载音频文件
let audioLoaded = false;
let audioBuffer = null;
audioLoader.load("/Beats.mp3", function (buffer) {
console.log("音频加载完成");
audioBuffer = buffer;
audioLoaded = true;
});
// 创建音频分析器,用于获取音频频率数据
const analyser = new THREE.AudioAnalyser(sound, 32);
- AudioListener:放在相机上,模拟现实中的“收听者位置”,也可做 3D 音效等。
- Audio:将该 Listener 传给音频对象后,就能进行播放、暂停、音量设置等操作。
- AudioLoader:加载本地或线上音频文件(.mp3、.wav 等)。
- AudioAnalyser:提供
getAverageFrequency()
等方法,用来实时获取当前音频的频谱信息。
6.1 在用户点击后播放音频
为了满足现代浏览器策略,需要用户交互后才能播放声音:
// 用户点击页面后,若音频已加载且未播放,则开始播放
document.addEventListener("click", function () {
if (audioLoaded && !sound.isPlaying) {
tryPlayAudio();
}
});
function tryPlayAudio() {
if (!audioLoaded) return;
try {
sound.setBuffer(audioBuffer);
sound.setLoop(true);
sound.play();
console.log("音频开始播放");
} catch (error) {
console.error("音频播放失败:", error);
}
}
7. 动画循环
const clock = new THREE.Clock();
function animate() {
// 更新时间
uniforms.u_time.value = clock.getElapsedTime();
// 更新频率变量
uniforms.u_frequency.value = analyser.getAverageFrequency();
// 使用效果合成器渲染场景(含后期处理)
bloomComposer.render();
// 请求下一帧
requestAnimationFrame(animate);
}
clock.getElapsedTime()
:获取从场景开始渲染到现在的总时长(秒)。analyser.getAverageFrequency()
:返回当前音频的频率值,会随音乐波动。- 每帧中都更新着色器的
u_time
和u_frequency
,就能使顶点产生“随音乐跳动”或波动的动态变形。
8. 自适应窗口尺寸
window.addEventListener("resize", function () {
// 更新相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
// 更新渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
// 更新效果合成器尺寸
bloomComposer.setSize(window.innerWidth, window.innerHeight);
});
- 监听
resize
事件,每次改变窗口大小时都要同时更新相机和渲染器、后期处理器的尺寸。 - 如果不做这个步骤,改变窗口后画面会扭曲或出现空白区域。
代码
https://github.com/calmound/threejs-demo/tree/main/ballnoise (opens in a new tab)