Three.js案例
使用Simplex Noise 实现动态立方体网格

使用 Three.js 和 Simplex Noise 实现动态立方体网格

本文将介绍一段使用 Three.js 配合 Simplex Noise 的示例代码,展示如何创建一个动态跳动的立方体网格,并通过调试面板 (dat.GUI) 来自由调整波动频率、振幅以及透明度等参数。

一、项目简介

在这个示例中,我们主要使用了以下技术与库:

  1. Three.js:WebGL 3D 渲染库,用于快速搭建和渲染三维场景。
  2. SimplexNoise:一种相对经典的噪声生成算法,用来给立方体添加随机波动效果。
  3. dat.GUI:轻量级的参数控制面板库,可在浏览器界面实时调节场景中的参数并查看变化。
  4. OrbitControls:Three.js 官方提供的相机控制器,可用鼠标在场景中旋转、缩放、平移相机。

主要功能:在浏览器窗口中,动态生成由许多立方体(Cube)组成的网格,每个立方体的高度根据 Simplex 噪声发生上下浮动,通过 dat.GUI 可以实时调节噪声频率、振幅以及立方体的透明度等参数。

二、代码解析

下面代码的结构大致可以拆分为如下几个部分:

  1. 场景与渲染器的初始化
  2. 相机设置
  3. 灯光设置
  4. 立方体网格及其噪声动画
  5. OrbitControls 与实时渲染
  6. 窗口自适应

让我们结合关键代码段进行说明。

1. 初始化场景与渲染器

var scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
 
var canvas = document.getElementById("canvas"),
  renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: false }),
  WIDTH = document.documentElement.clientWidth,
  HEIGHT = document.documentElement.clientHeight;
  • scene:创建一个场景(Scene),相当于整个三维世界的容器。
  • scene.background = new THREE.Color(0x000000):设置场景背景颜色为黑色。
  • renderer:创建一个 WebGL 渲染器,将内容最终绘制到指定的 canvas 上。
  • WIDTHHEIGHT:获取浏览器窗口的宽高,用于让渲染器充满整个屏幕。

接着:

renderer.setSize(WIDTH, HEIGHT);

让渲染器尺寸匹配当前浏览器窗口,保证显示的图像不被拉伸或裁剪。

2. Camera(相机)设置

var camera = new THREE.PerspectiveCamera(45, WIDTH / HEIGHT, 0.1, 100);
 
camera.position.x = 0;
camera.position.y = 8;
camera.position.z = population.z;
 
scene.add(camera);
  • PerspectiveCamera:透视相机,可设置视野角度(45°),宽高比(WIDTH / HEIGHT),以及可视范围(0.1 ~ 100)。
  • 将相机放置在场景中,调整 camera.position 得到较好的观察角度。

3. 灯光设置

var ambiantlight = new THREE.AmbientLight(0x555555);
scene.add(ambiantlight);
 
var light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 30, 0);
scene.add(light);
 
var light2 = new THREE.DirectionalLight(0xffffff, 1);
light2.position.set(30, 30, 0);
scene.add(light2);
  • AmbientLight:环境光,照亮场景里所有对象,颜色设置较暗(0x555555),让场景有柔和的全局光线。
  • DirectionalLight:平行光,类似太阳光源,可设置不同位置来打亮立方体表面,从而形成明暗对比。

4. 立方体网格与 Simplex 噪声动画

var shapes = [],
  population = { x: 35, z: 35 },
  simplex = new SimplexNoise(),
  iteration = 0,
  params = {
    simplexVariation: 0.05,
    simplexAmp: 2.5,
    opacity: 0.1,
  };
  • shapes:用于存放所有立方体对象。
  • population:表示立方体网格的尺寸;在 X 方向放 35 个,Z 方向放 35 个。
  • simplex:创建一个 SimplexNoise 的实例。
  • iteration:用作动画帧计数,通过它让噪声数据随时间变化。
  • params:将一些可调参数封装成对象,后续会用 dat.GUI 来绑定这些参数。

随后使用 dat.GUI 创建可视化参数面板:

var gui;
gui = new dat.GUI();
gui.add(params, "simplexVariation").min(0).max(0.2).step(0.0001).name("Frequency");
gui.add(params, "simplexAmp").min(0).max(10).step(0.1).name("Amplitude");
gui.add(params, "opacity").min(0.01).max(0.5).name("Alpha");
gui.open();
  • Frequency(simplexVariation:频率,决定噪声空间取样的“疏密”。
  • Amplitude(simplexAmp:振幅,决定噪声对立方体 Y 轴高度影响的幅度大小。
  • Alpha(opacity:立方体材质的不透明度。

4.1 立方体类的定义

function Cube(x, z) {
  this.speed = Math.floor(Math.random() * 100) / 100;
  this.geometry = new THREE.BoxGeometry(1, 1, 1);
  this.material = new THREE.MeshPhongMaterial({
    color: 0x2bc1ff,
    reflectivity: 0,
    transparent: true,
    opacity: params.opacity,
    shininess: 100,
    specular: 0x00ffff,
    shading: THREE.FlatShading,
  });
  this.mesh = new THREE.Mesh(this.geometry, this.material);
  this.mesh.position.x = x;
  this.mesh.position.y = 0;
  this.mesh.position.z = z;
}
  • Cube 构造函数中创建了一个 BoxGeometry (1x1x1) 和 MeshPhongMaterial,并将两者生成一个 Mesh 供场景使用。
  • speed 参数在这里并没被大范围使用,但它可以为立方体在动画中添加个性化速度。

4.2 立方体的移动(噪声驱动)

Cube.prototype.move = function () {
  this.material.opacity = params.opacity;
  this.mesh.position.y =
    simplex.noise4d(
      this.mesh.position.x * params.simplexVariation,
      this.mesh.position.y * params.simplexVariation,
      this.mesh.position.z * params.simplexVariation,
      iteration / 100
    ) * params.simplexAmp;
};
  • 这里使用了 4D 噪声 noise4d(...)。之所以是四维,是因为前三个维度通常映射到空间坐标 (x, y, z),第四维度用 iteration / 100 来模拟时间,令噪声值随动画帧递增而变化。
  • 每次调用 move() 方法,会根据当前的 paramsiteration,重新计算立方体在 Y 方向上的位置。

4.3 批量生成立方体

for (var i = population.x * -0.5; i <= population.x / 2; i++) {
  for (var u = population.z * -0.5; u <= population.z / 2; u++) {
    shapes[shapes.length] = new Cube(i, u);
    scene.add(shapes[shapes.length - 1].mesh);
  }
}
  • 外层循环 i 控制 X 方向的位置,内层循环 u 控制 Z 方向的位置。
  • 每个立方体中心点的横坐标 (x, z) 都是从 -population.x*0.5 ~ population.x*0.5-population.z*0.5 ~ population.z*0.5 之间的整数值。
  • 每创建一个立方体,就将其 mesh 添加到场景中。

5. OrbitControls 与场景动画

camera.lookAt(scene);
renderer.render(scene, camera);
 
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.enableZoom = true;
  • OrbitControls 可以让用户使用鼠标自由旋转、平移以及缩放相机。
  • enableDamping 能为镜头移动添加一些阻尼,使操作变得平滑。
  • dampingFactor 控制了阻尼程度。

5.1 动画主循环

function animate() {
  iteration++;
  requestAnimationFrame(animate);
 
  for (var i in shapes) {
    shapes[i].move();
  }
 
  renderer.render(scene, camera);
}
animate();
  • 在每一帧动画中:
    1. iteration 自增,让噪声随着时间变化。
    2. 遍历所有立方体,调用 move() 更新它们的 Y 坐标。
    3. 使用 renderer.render 将场景和相机渲染到屏幕。

6. 窗口自适应

window.addEventListener("resize", function () {
  WIDTH = window.innerWidth;
  HEIGHT = window.innerHeight;
  renderer.setSize(WIDTH, HEIGHT);
  camera.aspect = WIDTH / HEIGHT;
  camera.updateProjectionMatrix();
});

当浏览器窗口尺寸发生变化时,我们需要:

  1. 更新渲染器宽高(renderer.setSize)。
  2. 根据新的宽高比更新相机(camera.aspect),并执行 updateProjectionMatrix() 以应用修改。

这样就能在窗口大小变化时保持正确的可视范围和图像比例。

三、运行效果与参数调节

在成功运行代码后,你会看到一个由蓝色半透明立方体组成的网格,它们根据噪声生成上下起伏的高度变化。通过 dat.GUI 面板,你可以实时调节:

  1. Frequency(频率):在噪声采样坐标中,越大的 simplexVariation 值,噪声模式越密集,立方体随时间的抖动会更细碎。
  2. Amplitude(振幅):影响立方体上下浮动的幅度,越大,立方体分布的垂直落差越明显。
  3. Alpha(透明度):控制立方体材质的不透明度,使得场景更具科技或未来感。

可以结合 OrbitControls 在场景中拖拽、缩放、旋转视角,获得不同视野下的动态美感。

四、SimplexNoise

这里重点在讲解下噪声算法,它是这个动画效果的重点。

SimplexNoise 是一种噪声算法,与经典的 PerlinNoise 都能产生自然且平滑的随机值,但 SimplexNoise 在高维度运算时表现更好,计算效率也通常更高。它可用于模拟诸如地形起伏、波浪、云层运动等自然现象。

在视觉上,噪声函数看起来像是随机数,但与真正的随机数不同的是,它在空间中具有平滑度和连续性(Spatial Smoothness)。也就是说,随着输入参数小幅变化,噪声值也会随之平滑过渡,从而可用于模拟流动或渐变的自然效果。

在这段代码中,SimplexNoise 主要负责让所有立方体(Cubes)呈现动态“波动”效果。

在这份代码中,你会看到一个 4D 噪声函数的调用:

simplex.noise4d(xCoord, yCoord, zCoord, wCoord);

具体来说,代码中的这四个参数含义通常可对应如下维度:

  1. xCoord:网格点在 X 方向上的坐标(经过频率缩放)。
  2. yCoord:网格点在 Y 方向上的坐标(这里也进行了缩放,用于让 Y 作为噪声采样的一部分)。
  3. zCoord:网格点在 Z 方向上的坐标(同样经过频率缩放)。
  4. wCoord:时间维度(或其他控制维度),让噪声在动画过程中变化流动。

因为噪声函数在 3D (x, y, z) 空间中能生成一个平滑的值,而第四维度可以被用作**“时间”**输入。当第四维度发生变化时,噪声在 3D 空间中的取值也会相应改变,从而产生流动或波浪式的动画效果。

代码中的具体逻辑

代码位置

在立方体对象(Cube)中,有这样一个方法:

Cube.prototype.move = function () {
  this.material.opacity = params.opacity;
  this.mesh.position.y =
    simplex.noise4d(
      this.mesh.position.x * params.simplexVariation,
      this.mesh.position.y * params.simplexVariation,
      this.mesh.position.z * params.simplexVariation,
      iteration / 100
    ) * params.simplexAmp;
};

这里对每个立方体在动画帧中调用一次 move(),以更新它们的 Y 轴高度。重点关注 noise4d 的四个输入参数:

  1. this.mesh.position.x * params.simplexVariation
  2. this.mesh.position.y * params.simplexVariation
  3. this.mesh.position.z * params.simplexVariation
  4. iteration / 100

params.simplexVariation

  • 意义:它是一个频率参数(代码中的 Frequency),会把立方体的坐标缩放到噪声的采样空间。
  • 效果:当 params.simplexVariation 值较大时,输入到噪声函数的值变化幅度也会增大,噪声结果随坐标的变化会更“密集”,最终呈现出更频繁的上下波动。

iteration / 100

  • 意义:这一项通常可以理解为“时间”或动画帧数的输入。
  • 效果:在每帧 animate() 调用中,iteration++ 会不断增加,所以 noise4d 的第 4 个参数随时间推移而变化,从而让噪声的采样值随时间产生连续变化,形成可视化的“波动”。

也就是说,假如你把第四个维度改为一个固定值,那么所有立方体的高度会恒定在某种模式,不会随时间变化。而加入第四维后,整个噪声图像随时间在四维空间里“前进”,形成了动态效果。

params.simplexAmp

  • 意义:这是振幅(Amplitude),表示将噪声结果值再乘上一个因子,决定了立方体上下移动的最大高度范围。
  • 效果:当 params.simplexAmp 增大时,立方体在 Y 轴的起伏就更为显著;数值过小时,立方体上下移动会更轻微。

为什么要使用 4D 噪声?

三维坐标 + 时间 是一种常见的做法。例如,你想做一个在 3D 空间中时刻变化的云层效果,就可以把 x、y、z 看成空间坐标,用噪声来控制云层密度或地形高度,而第四个参数 w 则作为时间输入,这样云层就“飘动”起来、地形就“起伏”起来。

在本示例里,将 x、y、z 作为立方体网格本身的坐标,witeration / 100 来控制时间流动,达成了一种如水波或地面起伏的效果。

SimplexNoise 与 PerlinNoise 的差异

  • PerlinNoise:经典的 Ken Perlin 在 1983 年发明的噪声算法。适合生成平滑的过渡,但在更高维时计算复杂度会增加,也会出现一些格子状伪影。
  • SimplexNoise:改进自 PerlinNoise,可在更高维度下保持良好的性能,并且减少伪影现象,计算效率更高,也更易于扩展到 4D、5D 等。

在这个项目中,直接使用 SimplexNoise 相比 PerlinNoise 可以获得更自然和更高效率的过渡效果,特别是对于 4D 采样时的实时动画场景会更友好。

关于 sample-noise 我在网上找到了这份代码:https://gist.github.com/banksean/304522,不过这份代码只有3d (opens in a new tab)

在 codepen 作者使用的是哪份代码,没有找到源文件引入,通过 network 找到,倒是可以看到,他在上面的 github 的代码增加 noise4d 的逻辑,大家也可以使用 https://www.npmjs.com/package/simplex-noise (opens in a new tab) 这个 npm 库来替代。

代码

https://codepen.io/calmound/pen/gbOPXER (opens in a new tab)