昨天看到一个网站是2维的柱形图随着音乐节奏起伏,我突然想到二维形状可以起伏,三维应该也可以,最终原理应该都是将音乐频谱分解为数据,通过数据的切换不断地改变图形的形状来实现,因为找了分析音乐频谱的源码,解析完,再通过threejs的动画实现了3D场景下的模型随着音乐的节奏而变化,下面做详细步骤的说明:
这只是做个简单的demo,主要是有了音乐的数据,后面的效果可以任意发挥,首先需要新建一个场景,还是之前的老样子,创建场景,相机,灯光,渲染器等,代码如下:
initScene(){
scene = new THREE.Scene();
},
initCamera(){
this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
this.camera.position.set(200,400,200);
},
initLight(){
//添加两个平行光
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight1.position.set(300,300,600)
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight2.position.set(600,200,600)
scene.add(directionalLight2);
},
initFloor(){
let floorGeometry = new THREE.BoxGeometry( 200,1,200);
let floorMaterial = new THREE.MeshBasicMaterial({color:'#03133E'});
let floor = new THREE.Mesh( floorGeometry, floorMaterial );
floor.position.set(0,2,0)
scene.add(floor)
},
initRenderer(){
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.container = document.getElementById("container")
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setClearColor('#AAAAAA', 1.0);
this.container.appendChild(this.renderer.domElement);
},
initControl(){
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.maxPolarAngle = Math.PI / 2.2; // // 最大角度
},
initAnimate() {
requestAnimationFrame(this.initAnimate);
this.renderer.render(scene, this.camera);
}
然后在地板上放小方块,如果直接循环放置会放在同一个位置重叠,因为这里要在循环里控制每个小方块之间的距离,大概意思就是每个小方块长宽高为10,每个小方块间隔1,且每铺满11个小方块为一行后就再换一行重头平铺,每个方块的高度设置为0,水平位置为循环中计算出来的位置,最终全部平铺开来,每个都加入到场景中等会用音乐的节奏来控制小方块的跳动,但是经过测试太多的小方块会导致卡顿,(因为模型和修改模型位置和材质都会消耗性能,且因为方块独立跳动没办法进行合并),这里选择用121个小方块,做成11*11的方阵。
initBox(){
let x = -60; let z = -60;
for (let i = 0; i < 121; i++) {
if(i%11 === 0){
x = x + 11
z = -60
}
z = z + 11
const geometry = new THREE.BoxGeometry(10, 10, 10);
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00
});
let boxMesh = new THREE.Mesh(geometry, material);
this.boxMeshs.push(boxMesh)
this.boxMeshs[i].position.set(x, 0, z);
scene.add(this.boxMeshs[i]);
}
},
然后就需要去加载音乐了,音乐是设置在html中的audio标签,通过js获取标签元素来获取到连接的音乐资源,通过加载到的音乐分析,并获取他的音乐节奏,这部分我直接贴出代码,就不做详细解释了,如果有问题可以私信我。
initRound(){
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();
if(!this.source){
this.source = audioCtx.createMediaElementSource(this.myAudio);//通过audio标签获取播放源
this.analyser = audioCtx.createAnalyser(); // 创建AnalyserNode 用于分析音频频谱的节点
this.source.connect(this.analyser);
this.analyser.connect(audioCtx.destination);
let bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(bufferLength);
}
},
此时的场景如下:
下面就是关键的步骤,通过不断更新的音乐节奏来更新小方块的位置,这里是在渲染动画里去实现,在循环中不断通过this.analyser.getByteFrequencyData(this.dataArray);获取到新的音乐数据,然后根据dataArray的值设置方块的位置,这里是直接设置方块的所在高度,为了更好看的效果还加了颜色设置,通过不同高度切换不同的RGB颜色,并赋值给对应的小方块材质,
initAnimate() {
requestAnimationFrame(this.initAnimate);
this.renderer.render(scene, this.camera);
if(this.analyser){
this.analyser.getByteFrequencyData(this.dataArray);
for (let i = 0; i < this.dataArray.length-1; i++) {
if(this.boxMeshs[i]){
let x = this.boxMeshs[i].position.x
let z = this.boxMeshs[i].position.z
this.boxMeshs[i].position.set(x,this.dataArray[i],z)
this.boxMeshs[i].geometry.parameters.width = this.dataArray[i]
this.boxMeshs[i].material.color = new THREE.Color(0.8,this.dataArray[i],this.dataArray[i]);
let r =0, g = 0, b = 0;
if(this.dataArray[i]<85){
r = g = b = this.dataArray[i] * 3
}else if(this.dataArray[i]<170){
r = g = (this.dataArray[i] - 85) * 2
b = 255
}else{
r = (this.dataArray[i] - 170) * 3
g = b = 255
}
this.boxMeshs[i].material.color = new THREE.Color('rgb('+r+', '+g+', '+b+')')
}
}
}
},
基本的代码就这些了,下面附上完整代码
<template>
<div>
<div style="position: absolute;top:10px;left:40%;z-index: 100">
<audio id='myAudio' controls>
<source src="/static/video/music.mp3" type="audio/mp3">
</audio>
</div>
<div id="container"></div>
</div>
</template>
<script>
import * as THREE from 'three'
import {OrbitControls} from "three/addons/controls/OrbitControls";
let scene;
export default {
name: "agv-single",
data() {
return{
camera:null,
cameraCurve:null,
renderer:null,
container:null,
controls:null,
myAudio:null,
analyser:null,
dataArray:[],
boxMeshs:[],
source:null,
}
},
methods:{
initScene(){
scene = new THREE.Scene();
},
initCamera(){
this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
this.camera.position.set(200,400,200);
},
initLight(){
//添加两个平行光
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight1.position.set(300,300,600)
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight2.position.set(600,200,600)
scene.add(directionalLight2);
},
initFloor(){
let floorGeometry = new THREE.BoxGeometry( 200,1,200);
let floorMaterial = new THREE.MeshBasicMaterial({color:'#03133E'});
let floor = new THREE.Mesh( floorGeometry, floorMaterial );
floor.position.set(0,2,0)
scene.add(floor)
},
initRound(){
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();
if(!this.source){
this.source = audioCtx.createMediaElementSource(this.myAudio);//通过audio标签获取播放源
this.analyser = audioCtx.createAnalyser(); // 创建AnalyserNode 用于分析音频频谱的节点
this.source.connect(this.analyser);
this.analyser.connect(audioCtx.destination);
let bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(bufferLength);
}
},
initBox(){
let x = -60; let z = -60;
for (let i = 0; i < 121; i++) {
if(i%11 === 0){
x = x + 11
z = -60
}
z = z + 11
const geometry = new THREE.BoxGeometry(10, 10, 10);
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00
});
let boxMesh = new THREE.Mesh(geometry, material);
this.boxMeshs.push(boxMesh)
this.boxMeshs[i].position.set(x, 0, z);
scene.add(this.boxMeshs[i]);
}
},
initRenderer(){
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.container = document.getElementById("container")
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setClearColor('#AAAAAA', 1.0);
this.container.appendChild(this.renderer.domElement);
},
initControl(){
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.maxPolarAngle = Math.PI / 2.2; // // 最大角度
},
initAnimate() {
requestAnimationFrame(this.initAnimate);
this.renderer.render(scene, this.camera);
if(this.analyser){
this.analyser.getByteFrequencyData(this.dataArray);
for (let i = 0; i < this.dataArray.length-1; i++) {
if(this.boxMeshs[i]){
let x = this.boxMeshs[i].position.x
let z = this.boxMeshs[i].position.z
this.boxMeshs[i].position.set(x,this.dataArray[i],z)
this.boxMeshs[i].geometry.parameters.width = this.dataArray[i]
this.boxMeshs[i].material.color = new THREE.Color(0.8,this.dataArray[i],this.dataArray[i]);
let r =0, g = 0, b = 0;
if(this.dataArray[i]<85){
r = g = b = this.dataArray[i] * 3
}else if(this.dataArray[i]<170){
r = g = (this.dataArray[i] - 85) * 2
b = 255
}else{
r = (this.dataArray[i] - 170) * 3
g = b = 255
}
this.boxMeshs[i].material.color = new THREE.Color('rgb('+r+', '+g+', '+b+')')
}
}
}
},
initPage(){
this.initScene();
this.initCamera();
this.initLight();
this.initFloor();
this.initRenderer();
this.initControl();
this.initBox();
this.initAnimate();
}
},
mounted() {
this.myAudio = document.querySelector('#myAudio');
this.myAudio.addEventListener('play', () => {
this.initRound();
})
this.initPage()
}
}
</script>
<style scoped>
#container{
position: absolute;
width:100%;
height:100%;
overflow: hidden;
}
</style>
效果图如下