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

相关推荐
Мартин.2 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。3 小时前
案例-表白墙简单实现
前端·javascript·css
数云界3 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd3 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常3 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer3 小时前
Vite:为什么选 Vite
前端
小御姐@stella3 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing3 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd3 小时前
前端知识汇总(持续更新)
前端
万叶学编程6 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js