继续玩味之前写的音乐频谱作品,将原来在Canvas标签上的 作图利用Three.js让它通过WebGL呈现,这样就打造出了一个全立体感的频谱效果了。
项目详情及源码
项目GitHub地址:https://github.com/Wayou/3D_Audio_Spectrum_VIsualizer/tree/master
在线演示地址:http://wayou.github.io/3D_Audio_Spectrum_VIsualizer
如果你想的话,可以从这里下载示例音乐:http://pan.baidu.com/s/1eQqqSfS
Note:
- 可以直接点击'play default' 播放自带的音乐,神探夏洛克插曲,如果你也看了的话,听着应该会有感的
- 支持文件拖拽进行播放,将音频文件拖拽到页面即可
- 也可以通过文件上传按钮选择一个音频文件进行播放
- 鼠标拖拽可以移动镜头变换视野
- 鼠标滚轮可以进行缩放
- 右上角的控制面板可以进行一些外观及镜头上的设置,可以自己探索玩玩
利用Three.js呈现
关于音频处理方面的逻辑基本和之前介绍HTML5 Audio API那篇博客里讲的差不多,差别只在这个项目里面将频谱的展示从2d的canvas换成3d的WebGL进行展示,使用的是Three.js。所以只简单介绍关于3d场景方面的构建,具体实现可以访问项目GitHub页面下载源码。
构建跃动的柱条
每根绿色柱条是一个CubeGeometry,柱条上面的盖子也是CubeGeometry,只是长度更短而以,同时使用的是白色。
//创建绿色柱条的形状 var cubeGeometry = new THREE.CubeGeometry(MWIDTH, 1, MTHICKNESS); //创建绿色柱条的材质 var cubeMaterial = new THREE.MeshPhongMaterial({ color: 0x01FF00, ambient: 0x01FF00, specular: 0x01FF00, shininess: 20, reflectivity: 5.5 }); //创建白色盖子的形状 var capGeometry = new THREE.CubeGeometry(MWIDTH, 0.5, MTHICKNESS); //创建白色盖子的材质 var capMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, ambient: 0x01FF00, specular: 0x01FF00, shininess: 20, reflectivity: 5.5 });
上面只是创建了形状及材质,需要将这两者组合在一起形成一个模型,才是我们看到的实际物体。下面通过一个循环创建了一字排开的柱条和对应的盖子,然后添加到场景中。
//创建一字排开的柱条和盖子,并添加到场景中 for (var i = METERNUM - 1; i >= 0; i--) { var cube = new THREE.Mesh(cubeGeometry, cubeMaterial); cube.position.x = -45 + (MWIDTH + GAP) * i; cube.position.y = -1; cube.position.z = 0.5; cube.castShadow = true; cube.name = 'cube' + i; scene.add(cube); var cap = new THREE.Mesh(capGeometry, capMaterial); cap.position.x = -45 + (MWIDTH + GAP) * i; cap.position.y = 0.5; cap.position.z = 0.5; cap.castShadow = true; cap.name = 'cap' + i; scene.add(cap); };
注意到我们为每个物体指定了名称以方便之后获取该物体。
添加动画
动画部分同时是使用requestAnimation,根据传入的音频分析器(analyser)的数据来更新每根柱条的长度。
var renderAnimation = function() { if (analyser) { //从音频分析器中获取数据 var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / METERNUM); //更新每根柱条的高度 for (var i = 0; i < METERNUM; i++) { var value = array[i * step] / 4; value = value < 1 ? 1 : value; var meter = scene.getObjectByName('cube' + i, true); meter.scale.y = value; } }; //重新渲染画面 render.render(scene, camera); requestAnimationFrame(renderAnimation); }; requestAnimationFrame(renderAnimation);
对于白色盖子的处理稍微不同,因为它是缓慢下落的,不能使用及时送达的音频数据来更新它。实现的方式是每次动画更新中检查当前柱条的高度与前一时刻盖子的高度,看谁大,如果柱条更高,则盖子使用新的高度,否则盖子高度减1,这样就实现了缓落的效果。
var renderAnimation = function() { if (analyser) { var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / METERNUM); for (var i = 0; i < METERNUM; i++) { var value = array[i * step] / 4; value = value < 1 ? 1 : value; var meter = scene.getObjectByName('cube' + i, true), cap = scene.getObjectByName('cap' + i, true); meter.scale.y = value; //计算柱条边沿尺寸以获得高度 meter.geometry.computeBoundingBox(); height = (meter.geometry.boundingBox.max.y - meter.geometry.boundingBox.min.y) * value; //将柱条高度与盖子高度进行比较 if (height / 2 > cap.position.y) { cap.position.y = height / 2; } else { cap.position.y -= controls.dropSpeed; }; } }; //重新渲染画面 render.render(scene, camera); requestAnimationFrame(renderAnimation); }; requestAnimationFrame(renderAnimation);
镜头控制
镜头的控制使用的是与Three.js搭配的一个插件ObitControls.js,如果你下载了Three.js的源码可以在里面找到。只需获取一个鼠标拖动的前后时间差,然后在动画循环中调用插件进行画面更新即可。
var orbitControls = new THREE.OrbitControls(camera); orbitControls.minDistance = 50; orbitControls.maxDistance = 200; orbitControls.maxPolarAngle = 1.5; var renderAnimation = function() { var delta = clock.getDelta(); orbitControls.update(delta); if (analyser) { var array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); var step = Math.round(array.length / METERNUM); for (var i = 0; i < METERNUM; i++) { var value = array[i * step] / 4; value = value < 1 ? 1 : value; var meter = scene.getObjectByName('cube' + i, true), cap = scene.getObjectByName('cap' + i, true); meter.scale.y = value; //计算柱条边沿尺寸以获得高度 meter.geometry.computeBoundingBox(); height = (meter.geometry.boundingBox.max.y - meter.geometry.boundingBox.min.y) * value; //将柱条高度与盖子高度进行比较 if (height / 2 > cap.position.y) { cap.position.y = height / 2; } else { cap.position.y -= controls.dropSpeed; }; } }; //重新渲染画面 render.render(scene, camera); requestAnimationFrame(renderAnimation); }; requestAnimationFrame(renderAnimation);
注意到在实例化一个ObitControls后,进行了一些角度和镜头伸缩方面的设置,限制了用户把画面翻转到平面的底部,也保证了镜头在伸缩时不会太远及太近。
参数控制
右上角的控制面板可以进行画面的一些参数更改,使用的是谷歌员工创建的一个插件dat.gui.js 。
首先需要定义一个包含全部需要控制的参数的对象:
var controls = new function() { this.capColor = 0xFFFFFF; this.barColor = 0x01FF00; this.ambientColor = 0x0c0c0c; this.dropSpeed = 0.1; this.autoRotate = false; };
然后实例化一个控制器,将这个对象及相应参数进行绑定:
var gui = new dat.GUI(); //添加盖子下降速度的控制 gui.add(controls, 'dropSpeed', 0.1, 0.5); //盖子颜色控制 gui.addColor(controls, 'capColor').onChange(function(e) { scene.children.forEach(function(child) { if (child.name.indexOf('cap') > -1) { child.material.color.setStyle(e); child.material.ambient = new THREE.Color(e) child.material.emissive = new THREE.Color(e) child.material.needsUpdate = true; } }); }); //柱条颜色控制 gui.addColor(controls, 'barColor').onChange(function(e) { scene.children.forEach(function(child) { if (child.name.indexOf('cube') > -1) { child.material.color.setStyle(e); child.material.ambient = new THREE.Color(e) child.material.emissive = new THREE.Color(e) child.material.needsUpdate = true; } }); }); //镜头自动移动控制 gui.add(controls, 'autoRotate').onChange(function(e) { orbitControls.autoRotate = e; });
总结
完成了主要功能,但没达到我预期的效果,我想的是把柱条做成发光的,调研了一下,需要用更复杂的材质,同时也不能用WebGL来渲染画面了,性能是一方面,同时也还没研究得那么深入,所以就先出了这个版本先。以后或许弄个水波效果。
REFERENCE
Offical Documentation: http://threejs.org/docs/
A Demo: http://srchea.com/blog/2013/05/experimenting-with-web-audio-api-three-js-webgl/
Another Example: https://github.com/arirusso/three-audio-spectrum
A Working Demo: http://badassjs.com/post/27056714305/plucked-html5-audio-editor-and-threeaudio-js
Dat GUI plugin: https://code.google.com/p/dat-gui/