发布网友 发布时间:2024-09-19 02:01
共1个回答
热心网友 时间:2024-09-19 02:47
最近听了一首很好听的歌《一路生花》,于是就想用Three.js做个音乐频谱的可视化,最终效果是这样的:
代码地址在这里:https://github.com/QuarkGluonPlasma/threejs-exercize
这个效果的实现能学到两方面的内容:
AudioContext对音频解码和各种处理
Three.js的3d场景绘制
那还等什么,我们开始吧。
思路分析要做音乐频谱可视化,首先要获取频谱数据,这个用AudioContext的api。
AudioContext的api可以对音频解码并对它做一系列处理,每一个处理步骤叫做一个Node。
我们这里需要解码之后用analyser来拿到频谱数据,然后传递给audioContext做播放。所以有三个处理节点:Source、Analyser、Destination
contextaudioCtx=newAudioContext();constsource=audioCtx.createBufferSource();constanalyser=audioCtx.createAnalyser();audioCtx.decodeAudioData(音频二进制数据,function(decodedData){source.buffer=decodedData;source.connect(analyser);analyser.connect(audioCtx.destination);});先对音频解码,创建BufferSource的节点来保存解码后的数据,然后传入Analyser获取频谱数据,最后传递给Destination来播放。
调用source.start()开始传递音频数据,这样analyser就能够拿到音乐频谱的数据了,Destination也能正常的播放。
analyser拿到音频频谱数据的api是这样的:
constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);每一次能拿到的frequencyData有1024个元素,可以按50个分为一份,算下平均值,这样只会有1024/50=21个频谱单元数据。
之后就可以用Three.js把这些频谱数据画出来了。
21个数值,可以绘制成21个立方体BoxGeometry,材质的话,用MeshPhongMaterial(因为这个反光的计算方式是一个姓冯的人提出来的,所以叫Phong),它的特点是表面可以反光,如果用MeshBasicMaterial,是不反光的。
之后加入花瓣雨效果,这个我们之前实现过,就是用Sprite(永远面向相机的一个平面)做贴图,然后一帧帧做位置的改变。
通过“漫天花雨”来入门Three.js
之后分别设置灯光、相机就可以了:
灯光我们用点光源PointLight,从一个位置去照射,配合Phong的材质可以做到反光的效果。
相机用透视相机PerspectiveCamera,它的特点是从一个点去看,会有近大远小的效果,比较有空间感。而正交相机OrthographicCamera因为是平行投影,就没有近大远小的效果,不管距离多远的物体都是一样大。
之后通过Renderer渲染出来,然后用requestAnimationFrame来一帧帧的刷新就可以了。
接下来我们具体写下代码:
代码实现我们先通过fetch拿到服务器上的音频文件,转成ArrayBuffer。
ArrayBuffer是JS语言提供的用于存储二进制数据的api,和它类似的还有Blob和Buffer,区别如下:
ArrayBuffer是JS语言本身提供的用于存储二进制数据的通用API
Blob是浏览器提供的API,用于文件处理
Buffer是Node.js提供的API,用于IO操作
这里,我们毫无疑问要用ArrayBuffer来存储音频的二进制数据。
fetch('./music/一路生花.mp3').then(function(response){if(!response.ok){thrownewError("HTTPerror,status="+response.status);}returnresponse.arrayBuffer();}).then(function(arrayBuffer){});然后用AudioContext的api做解码和后续处理,分为Source、Analyser、Destination3个处理节点:
letaudioCtx=newAudioContext();letsource,analyser;functiongetData(){source=audioCtx.createBufferSource();analyser=audioCtx.createAnalyser();returnfetch('./music/一路生花.mp3').then(function(response){if(!response.ok){thrownewError("HTTPerror,status="+response.status);}returnresponse.arrayBuffer();}).then(function(arrayBuffer){audioCtx.decodeAudioData(arrayBuffer,function(decodedData){source.buffer=decodedData;source.connect(analyser);analyser.connect(audioCtx.destination);});});};获取音频,用AudioContext处理之后,并不能直接播放,因为浏览器做了*。必须得用户主动做了一些操作之后,才能播放音频。
为了绕过这个*,我们监听mousedown事件,用户点击之后,就可以播放了。
functiontriggerHandler(){getData().then(function(){source.start(0);//从0的位置开始播放create();//创建Three.js的各种物体render();//渲染});document.removeEventListener('mousedown',triggerHandler)}document.addEventListener('mousedown',triggerHandler);之后可以创建3D场景中的各种物体:
创建立方体:
因为频谱为1024个数据,我们50个分为一组,就只需要渲染21个立方体:
constcubes=newTHREE.Group();constSTEP=50;constCUBE_NUM=Math.ceil(1024/STEP);for(leti=0;i<CUBE_NUM;i++){constgeometry=newTHREE.BoxGeometry(10,10,10);constmaterial=newTHREE.MeshPhongMaterial({color:'yellowgreen'});constcube=newTHREE.Mesh(geometry,material);cube.translateX((10+10)*i);cubes.add(cube);}cubes.translateX(-(10+10)*CUBE_NUM/2);scene.add(cubes);立方体的物体Mesh,分别设置几何体是BoxGeometry,长宽高都是10,材质是MeshPhongMaterial,颜色是黄绿色。
每个立方体要做下x轴的位移,最后整体的分组再做下位移,移动整体宽度的一半,达到居中的目的。
频谱就可以通过这些立方体来做可视化。
之后是花瓣,用Sprite创建,因为Sprite是永远面向相机的平面。贴上随机的纹理贴图,设置随机的位置。
constFLOWER_NUM=400;/***花瓣分组*/constpetal=newTHREE.Group();varflowerTexture1=newTHREE.TextureLoader().load("img/flower1.png");varflowerTexture2=newTHREE.TextureLoader().load("img/flower2.png");varflowerTexture3=newTHREE.TextureLoader().load("img/flower3.png");varflowerTexture4=newTHREE.TextureLoader().load("img/flower4.png");varflowerTexture5=newTHREE.TextureLoader().load("img/flower5.png");varimageList=[flowerTexture1,flowerTexture2,flowerTexture3,flowerTexture4,flowerTexture5];for(leti=0;i<FLOWER_NUM;i++){varspriteMaterial=newTHREE.SpriteMaterial({map:imageList[Math.floor(Math.random()*imageList.length)],});varsprite=newTHREE.Sprite(spriteMaterial);petal.add(sprite);sprite.scale.set(40,50,1);sprite.position.set(2000*(Math.random()-0.5),500*Math.random(),2000*(Math.random()-0.5))}scene.add(petal);分别把频谱的立方体和一堆花瓣加到场景中之后,就完成了物体的创建。
然后设置下相机,我们是使用透视相机,要分别指定视角的角度,最近和最远的距离,还有视区的宽高比。
之后设置下灯光,用点光源:
constpointLight=newTHREE.PointLight(0xffffff);pointLight.position.set(0,300,40);scene.add(pointLight);然后就可以用renderer来做渲染了,结合requestAnimationFrame做一帧帧的渲染。
constrenderer=newTHREE.WebGLRenderer();functionrender(){renderer.render(scene,camera);requestAnimationFrame(render);}render();在渲染的时候,每帧都要计算花瓣的位置,和频谱立方体的高度。
花瓣的位置就是不断下降,到了一定的高度就回到上面:
constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);0频谱立方体的话,要用analyser获取最新频谱数据,计算每个分组的平均值,然后设置到立方体的scaleY上。
//获取频谱数据constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);//计算每个分组的平均频谱数据constaverageFrequencyData=[];for(leti=0;i<frequencyData.length;i+=STEP){letsum=0;for(letj=i;j<i+STEP;j++){sum+=frequencyData[j];}averageFrequencyData.push(sum/STEP);}//设置立方体的scaleYfor(leti=0;i<averageFrequencyData.length;i++){cubes.children[i].scale.y=Math.floor(averageFrequencyData[i]*0.4);}还可以做下场景围绕X轴的渲染,每帧转一定的角度。
constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);2最后,加入轨道控制器就可以了,它的作用是可以用鼠标来调整相机的位置,调整看到的东西的远近、角度等。
constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);3最终效果就是这样的:花瓣纷飞,频谱立方体随音乐跳动。
完整代码提交到了github:
https://github.com/QuarkGluonPlasma/threejs-exercize
也在这里贴一份:
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>音乐频谱可视化</title><style>body{margin:0;overflow:hidden;}</style><scriptsrc="./js/three.js"></script><scriptsrc="./js/OrbitControls.js"></script></head><body><script>letaudioCtx=newAudioContext();letsource,analyser;functiongetData(){source=audioCtx.createBufferSource();analyser=audioCtx.createAnalyser();returnfetch('./music/一路生花.mp3').then(function(response){if(!response.ok){thrownewError("HTTPerror,status="+response.status);}returnresponse.arrayBuffer();}).then(function(arrayBuffer){audioCtx.decodeAudioData(arrayBuffer,function(decodedData){source.buffer=decodedData;source.connect(analyser);analyser.connect(audioCtx.destination);});});};functiontriggerHandler(){getData().then(function(){source.start(0);create();render();});document.removeEventListener('mousedown',triggerHandler)}document.addEventListener('mousedown',triggerHandler);constSTEP=50;constCUBE_NUM=Math.ceil(1024/STEP);constFLOWER_NUM=400;constwidth=window.innerWidth;constheight=window.innerHeight;constscene=newTHREE.Scene();constcamera=newTHREE.PerspectiveCamera(45,width/height,0.1,1000);constrenderer=newTHREE.WebGLRenderer();/***花瓣分组*/constpetal=newTHREE.Group();/***频谱立方体*/constcubes=newTHREE.Group();functioncreate(){constpointLight=newTHREE.PointLight(0xffffff);pointLight.position.set(0,300,40);scene.add(pointLight);camera.position.set(0,300,400);camera.lookAt(scene.position);renderer.setSize(width,height);document.body.appendChild(renderer.domElement)renderer.render(scene,camera)for(leti=0;i<CUBE_NUM;i++){constgeometry=newTHREE.BoxGeometry(10,10,10);constmaterial=newTHREE.MeshPhongMaterial({color:'yellowgreen'});constcube=newTHREE.Mesh(geometry,material);cube.translateX((10+10)*i);cube.translateY(1);cubes.add(cube);}cubes.translateX(-(10+10)*CUBE_NUM/2);varflowerTexture1=newTHREE.TextureLoader().load("img/flower1.png");varflowerTexture2=newTHREE.TextureLoader().load("img/flower2.png");varflowerTexture3=newTHREE.TextureLoader().load("img/flower3.png");varflowerTexture4=newTHREE.TextureLoader().load("img/flower4.png");varflowerTexture5=newTHREE.TextureLoader().load("img/flower5.png");varimageList=[flowerTexture1,flowerTexture2,flowerTexture3,flowerTexture4,flowerTexture5];for(leti=0;i<FLOWER_NUM;i++){varspriteMaterial=newTHREE.SpriteMaterial({map:imageList[Math.floor(Math.random()*imageList.length)],});varsprite=newTHREE.Sprite(spriteMaterial);petal.add(sprite);sprite.scale.set(40,50,1);sprite.position.set(2000*(Math.random()-0.5),500*Math.random(),2000*(Math.random()-0.5))}scene.add(cubes);scene.add(petal);}functionrender(){petal.children.forEach(sprite=>{sprite.position.y-=5;sprite.position.x+=0.5;if(sprite.position.y<-height/2){sprite.position.y=height/2;}if(sprite.position.x>1000){sprite.position.x=-1000;}});constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);constaverageFrequencyData=[];for(leti=0;i<frequencyData.length;i+=STEP){letsum=0;for(letj=i;j<i+STEP;j++){sum+=frequencyData[j];}averageFrequencyData.push(sum/STEP);}for(leti=0;i<averageFrequencyData.length;i++){cubes.children[i].scale.y=Math.floor(averageFrequencyData[i]*0.4);}constfrequencyData=newUint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(frequencyData);2renderer.render(scene,camera);requestAnimationFrame(render);}constfrequencyData=newUint8Array(analyser.frequencyBi