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编辑器功能详解🎯🎯🎯

相关推荐
zhanghaisong_201534 分钟前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
Eric_见嘉37 分钟前
真的能无限试(白)用(嫖)cursor 吗?
前端·visual studio code
DK七七1 小时前
多端校园圈子论坛小程序,多个学校同时代理,校园小程序分展示后台管理源码
开发语言·前端·微信小程序·小程序·php
老赵的博客1 小时前
QSS 设置bug
前端·bug·音视频
Chikaoya1 小时前
项目中用户数据获取遇到bug
前端·typescript·vue·bug
南城夏季1 小时前
蓝领招聘二期笔记
前端·javascript·笔记
Huazie1 小时前
来花个几分钟,轻松掌握 Hexo Diversity 主题配置内容
前端·javascript·hexo
NoloveisGod2 小时前
Vue的基础使用
前端·javascript·vue.js
GISer_Jing2 小时前
前端系统设计面试题(二)Javascript\Vue
前端·javascript·vue.js
海上彼尚2 小时前
实现3D热力图
前端·javascript·3d