Threejs 实现梦幻频谱可视化🧿🧿🧿

写在最前

上个礼拜无聊捣鼓了一下频谱可视化效果,分别用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中有很多种方法可以得到四边形, 如PlaneGeometryBoxGeometry

音浪功能另外一个就是动态高度 ,使用内置的几何体可通过修改缩放比例 去实现。但这种做法就太不优雅了,有没有其他的方法呢,那必然是 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 相关的文章,谢谢浏览!

🔗源码地址

往期链接:

🔗# Threejs 实现虚拟摇杆遨游星空 ✨✨

🔗# Threejs 让人眼前一亮的隧道穿越 🌌🌌🌌

🔗# 实现抛物线跳跃交互底部导航栏🎈🎈🎈

🔗# Threejs 中秋佳节感受闽南名俗 | 中秋博饼🥮🥮🥮

🔗# Threejs glTF编辑器功能详解🎯🎯🎯

相关推荐
玩电脑的辣条哥3 小时前
Python如何播放本地音乐并在web页面播放
开发语言·前端·python
ew452183 小时前
ElementUI表格表头自定义添加checkbox,点击选中样式不生效
前端·javascript·elementui
suibian52353 小时前
AI时代:前端开发的职业发展路径拓宽
前端·人工智能
Moon.93 小时前
el-table的hasChildren不生效?子级没数据还显示箭头号?树形数据无法展开和收缩
前端·vue.js·html
垚垚 Securify 前沿站3 小时前
深入了解 AppScan 工具的使用:筑牢 Web 应用安全防线
运维·前端·网络·安全·web安全·系统安全
工业甲酰苯胺6 小时前
Vue3 基础概念与环境搭建
前端·javascript·vue.js
mosquito_lover17 小时前
怎么把pyqt界面做的像web一样漂亮
前端·python·pyqt
柴柴的小记9 小时前
前端vue引入特殊字体不生效
前端·javascript·vue.js
柠檬豆腐脑10 小时前
从前端到全栈:新闻管理系统及多个应用端展示
前端·全栈
bin915310 小时前
DeepSeek 助力 Vue 开发:打造丝滑的颜色选择器(Color Picker)
前端·javascript·vue.js·ecmascript·deepseek