使用 Three.js 和 Simplex Noise 实现动态立方体网格
本文将介绍一段使用 Three.js 配合 Simplex Noise 的示例代码,展示如何创建一个动态跳动的立方体网格,并通过调试面板 (dat.GUI) 来自由调整波动频率、振幅以及透明度等参数。
一、项目简介
在这个示例中,我们主要使用了以下技术与库:
- Three.js:WebGL 3D 渲染库,用于快速搭建和渲染三维场景。
- SimplexNoise:一种相对经典的噪声生成算法,用来给立方体添加随机波动效果。
- dat.GUI:轻量级的参数控制面板库,可在浏览器界面实时调节场景中的参数并查看变化。
- OrbitControls:Three.js 官方提供的相机控制器,可用鼠标在场景中旋转、缩放、平移相机。
主要功能:在浏览器窗口中,动态生成由许多立方体(Cube)组成的网格,每个立方体的高度根据 Simplex 噪声发生上下浮动,通过 dat.GUI 可以实时调节噪声频率、振幅以及立方体的透明度等参数。
二、代码解析
下面代码的结构大致可以拆分为如下几个部分:
- 场景与渲染器的初始化
- 相机设置
- 灯光设置
- 立方体网格及其噪声动画
- OrbitControls 与实时渲染
- 窗口自适应
让我们结合关键代码段进行说明。
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
上。WIDTH
、HEIGHT
:获取浏览器窗口的宽高,用于让渲染器充满整个屏幕。
接着:
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()
方法,会根据当前的params
与iteration
,重新计算立方体在 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();
- 在每一帧动画中:
iteration
自增,让噪声随着时间变化。- 遍历所有立方体,调用
move()
更新它们的 Y 坐标。 - 使用
renderer.render
将场景和相机渲染到屏幕。
6. 窗口自适应
window.addEventListener("resize", function () {
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
});
当浏览器窗口尺寸发生变化时,我们需要:
- 更新渲染器宽高(
renderer.setSize
)。 - 根据新的宽高比更新相机(
camera.aspect
),并执行updateProjectionMatrix()
以应用修改。
这样就能在窗口大小变化时保持正确的可视范围和图像比例。
三、运行效果与参数调节
在成功运行代码后,你会看到一个由蓝色半透明立方体组成的网格,它们根据噪声生成上下起伏的高度变化。通过 dat.GUI 面板,你可以实时调节:
- Frequency(频率):在噪声采样坐标中,越大的
simplexVariation
值,噪声模式越密集,立方体随时间的抖动会更细碎。 - Amplitude(振幅):影响立方体上下浮动的幅度,越大,立方体分布的垂直落差越明显。
- Alpha(透明度):控制立方体材质的不透明度,使得场景更具科技或未来感。
可以结合 OrbitControls 在场景中拖拽、缩放、旋转视角,获得不同视野下的动态美感。
四、SimplexNoise
这里重点在讲解下噪声算法,它是这个动画效果的重点。
SimplexNoise 是一种噪声算法,与经典的 PerlinNoise 都能产生自然且平滑的随机值,但 SimplexNoise 在高维度运算时表现更好,计算效率也通常更高。它可用于模拟诸如地形起伏、波浪、云层运动等自然现象。
在视觉上,噪声函数看起来像是随机数,但与真正的随机数不同的是,它在空间中具有平滑度和连续性(Spatial Smoothness)。也就是说,随着输入参数小幅变化,噪声值也会随之平滑过渡,从而可用于模拟流动或渐变的自然效果。
在这段代码中,SimplexNoise 主要负责让所有立方体(Cubes)呈现动态“波动”效果。
在这份代码中,你会看到一个 4D 噪声函数的调用:
simplex.noise4d(xCoord, yCoord, zCoord, wCoord);
具体来说,代码中的这四个参数含义通常可对应如下维度:
- xCoord:网格点在 X 方向上的坐标(经过频率缩放)。
- yCoord:网格点在 Y 方向上的坐标(这里也进行了缩放,用于让 Y 作为噪声采样的一部分)。
- zCoord:网格点在 Z 方向上的坐标(同样经过频率缩放)。
- 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
的四个输入参数:
this.mesh.position.x * params.simplexVariation
this.mesh.position.y * params.simplexVariation
this.mesh.position.z * params.simplexVariation
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 作为立方体网格本身的坐标,w 由 iteration / 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)