写在最前
上个礼拜无聊捣鼓了一下频谱可视化效果,分别用canvas和threejs实现了部分已有的和自己捣鼓的特效,今天准备拿出来分享一下,将从0到1的去还原实现,话不多说直接开冲。
先上效果:
程序入口
js
let init = async () => {
initHalo();
initEarth();
initSpectrum();
initWater();
initBloomPass();
await loadMusic();
isLoading = false;
};
我们从上面的入口函数中可以看出拥有6个主函数,我们逐一进行解析
初始化频谱数据
HTML5的Web Audio Api提供了处理音频的能力,可以生成频率数据和波形数据,非常强大。
具体的介绍可参考网易云音乐前端团队写的 文章
波形图横坐标代表时间域、纵坐标代表振幅,频率图横坐标代表频率(低频和高频),纵坐标代表频率能量值
js
async function loadMusic() {
// 加载音乐
let audioContext = new (window.AudioContext || window.webkitAudioContext)();
const arrayBuffer = await loadSound("/src/views/spectrum/11582.mp3");
audioBufferSourceNode = audioContext.createBufferSource();
audioBufferSourceNode.connect(audioContext.destination);
const audioBuffer = await bufferToAudio(audioContext, arrayBuffer);
audioBufferSourceNode.buffer = audioBuffer;
analyser = audioContext.createAnalyser();
audioBufferSourceNode.connect(analyser);
}
通过分析器获取频率数据最终会得到一个频率数组 dataArray用于后面展示
analyser.getByteFrequencyData(dataArray);
音浪实现
threejs作为一个webgl 3D框架,它内置了很多方法,使我们能够很快的构建一个几何体。
上面的音浪看上去是由多个四边形组成,threejs中有很多种方法可以得到四边形, 如PlaneGeometry 、BoxGeometry。
音浪功能另外一个就是动态高度 ,使用内置的几何体可通过修改缩放比例 去实现。但这种做法就太不优雅了,有没有其他的方法呢,那必然是 BufferGeometry自定义几何体了。
我们首先先初始化一个 BufferGeometry 几何体
js
let initSpectrum = () => {
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array(900);
const color = new Float32Array(900);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(color, 3));
const material = new THREE.MeshBasicMaterial({
vertexColors: true,
});
spectrum = new THREE.Mesh(geometry, material);
scene.add(spectrum);
spectrum.position.set(0, 12, 0);
};
几何体最基本的信息是位置和颜色,我们只需要准备好顶点信息 和顶点颜色信息即可。
我们在学习webGL的过程中能知道 图形渲染是以最简单的几何体 ------------ 三角形 为基础的。所以我们以两个三角形去画四边形,顶点顺序为下图:
我们只需要确定间隔 gap 和四边形的宽度 width 与 是第i个四边形就很容易得出以下函数:
js
let width = 0.1;
let gap = 0.6;
let height = dataArray[i] / 100; // 频率数据
// 设置顶点位置
let halfLength = (gap * (dataArray.length - 1) + width) / 2;
// 左下
spectrum.geometry.attributes.position.array[18 * i + 0] =
-(width / 2) + i * gap - halfLength;
spectrum.geometry.attributes.position.array[18 * i + 1] = 0;
spectrum.geometry.attributes.position.array[18 * i + 2] = 0;
// 右下
spectrum.geometry.attributes.position.array[18 * i + 3] =
width / 2 + i * gap - halfLength;
spectrum.geometry.attributes.position.array[18 * i + 4] = 0;
spectrum.geometry.attributes.position.array[18 * i + 5] = 0;
// 右上
spectrum.geometry.attributes.position.array[18 * i + 6] =
width / 2 + i * gap - halfLength;
spectrum.geometry.attributes.position.array[18 * i + 7] = height;
spectrum.geometry.attributes.position.array[18 * i + 8] = 0;
// 左下
spectrum.geometry.attributes.position.array[18 * i + 9] =
-(width / 2) + i * gap - halfLength;
spectrum.geometry.attributes.position.array[18 * i + 10] = 0;
spectrum.geometry.attributes.position.array[18 * i + 11] = 0;
// 左上
spectrum.geometry.attributes.position.array[18 * i + 12] =
-(width / 2) + i * gap - halfLength;
spectrum.geometry.attributes.position.array[18 * i + 13] = height;
spectrum.geometry.attributes.position.array[18 * i + 14] = 0;
// 右上
spectrum.geometry.attributes.position.array[18 * i + 15] =
width / 2 + i * gap - halfLength;
spectrum.geometry.attributes.position.array[18 * i + 16] = height;
spectrum.geometry.attributes.position.array[18 * i + 17] = 0;
// 设置顶点颜色
spectrum.geometry.attributes.color.array[18 * i + 0] = 0;
spectrum.geometry.attributes.color.array[18 * i + 1] = 0;
spectrum.geometry.attributes.color.array[18 * i + 2] = 1;
spectrum.geometry.attributes.color.array[18 * i + 3] = 0;
spectrum.geometry.attributes.color.array[18 * i + 4] = 0;
spectrum.geometry.attributes.color.array[18 * i + 5] = 1;
spectrum.geometry.attributes.color.array[18 * i + 6] = 1;
spectrum.geometry.attributes.color.array[18 * i + 7] = 1;
spectrum.geometry.attributes.color.array[18 * i + 8] = 1;
spectrum.geometry.attributes.color.array[18 * i + 9] = 0;
spectrum.geometry.attributes.color.array[18 * i + 10] = 0;
spectrum.geometry.attributes.color.array[18 * i + 11] = 1;
spectrum.geometry.attributes.color.array[18 * i + 12] = 1;
spectrum.geometry.attributes.color.array[18 * i + 13] = 1;
spectrum.geometry.attributes.color.array[18 * i + 14] = 1;
spectrum.geometry.attributes.color.array[18 * i + 15] = 1;
spectrum.geometry.attributes.color.array[18 * i + 16] = 1;
spectrum.geometry.attributes.color.array[18 * i + 17] = 1;
至此已经完成了音浪的基础形态,后续动态高度则只需要修改相关顶点高度就好。
光环实现
首先我们观察光环的侧面
可以看到光环是由很多个圆圈组成的,由于透视像机近大远小的原理,呈现出来的效果就是一个圆环。
首先初始化 20 个自定义几何体:
js
async function initHalo() {
for (let i = 0; i < 20; i++) {
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array(150);
const color = new Float32Array(150);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
const material = new THREE.MeshBasicMaterial({
color: 0x77c5f4,
});
let curveObject = new THREE.Line(geometry, material);
haloGroup.add(curveObject);
curveObject.position.z = i / 2;
haloGroup.position.x = 0;
haloGroup.position.z = -5;
haloGroup.position.y = 0;
haloGroup.rotation.z = -Math.PI / 2;
}
scene.add(haloGroup);
}
再通过三角函数获取圆上的点坐标并修改顶点位置从而绘制出圆。
js
function haloAnimate(i) {
for (let n = 0; n < haloGroup.children.length; n++) {
let poiX =
Math.cos((((i * 360) / dataArray.length) * Math.PI) / 180) *
(dataArray[i] / 100 + 5);
let poiY =
Math.sin((((i * 360) / dataArray.length) * Math.PI) / 180) *
(dataArray[i] / 100 + 5);
haloGroup.children[n].geometry.attributes.position.array[i * 3] = poiX;
haloGroup.children[n].geometry.attributes.position.array[i * 3 + 1] = poiY;
haloGroup.children[n].geometry.attributes.position.array[i * 3 + 2] = 0;
haloGroup.children[n].geometry.attributes.position.needsUpdate = true;
}
}
这样我们的光环就完成啦
添加发光后期
后期特效是作品至关重要的,上一篇文章gltf编辑器我们有用到 outlinePass选中后期。今天用到的是发光特效,官方demo。
js
/**
* Bloom 发光后期
*/
function initBloomPass() {
let renderScene = new RenderPass(scene, camera);
var bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1,
0,
0,
);
composer = new EffectComposer(renderer);
composer.setSize(window.innerWidth, window.innerHeight);
composer.addPass(renderScene);
composer.addPass(bloomPass);
}
添加 Water
当发光特效遇到水,美的不可言喻。将几何体放入水流内一半,有一种鸿蒙Logo动画的美 😱😱
js
function initWater() {
const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
water = new Water(waterGeometry, {
textureWidth: 512,
textureHeight: 512,
waterNormals: new THREE.TextureLoader().load(
"/src/views/spectrum/61f013894d2f49f78af775f42ac6a085~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.awebp",
function (texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
}
),
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: 0xb7ffff,
distortionScale: 3.7,
fog: scene.fog !== undefined,
});
water.rotation.x = -Math.PI / 2;
water.position.y = -2;
scene.add(water);
}
这样,我们整个程序就大功告成啦
写在最后
我们通过本篇文章可以学习到 BufferGeometry、Web Audio Api、三角函数画圆、发光后期使用、水Shader的引用,相信大家看完以后拥有自己的理解与想法,看看是否能实现更加美丽的效果
喜欢的话帮忙点个赞 + 关注吧,将持续更新 Threejs 相关的文章,谢谢浏览!
🔗源码地址
往期链接: