动态线条
今天来看 Three.js 的官网案例,动态线条,通过这个案例你可以了解到以下内容:
- 如何生成五颜六色的线
- 线是如何按照一定规律进行动画的
- 线是如何包裹在立方体内
它通过组合使用 BufferGeometry
、LineBasicMaterial
、Line
、morphAttributes
等方法来实现最终效果。
二、如何生成五颜六色的线
要在 Three.js 中绘制线,我们通常会使用 BufferGeometry
(可管理大规模顶点数据),然后配合 LineBasicMaterial
。
-
首先定义
segments = 10000
变量,表示我们将产生 10000 个顶点,这些顶点会组成一条Line
,实际上能形成“多段”线。由于没有显式定义索引,Three.js 会按顶点的顺序依次连接它们。, -
然后,通过
positions
数组中存放了每个点的 xyz 坐标,这些坐标是从-r/2
到r/2
的范围内随机生成的,恰好对应一个边长为r
的立方体边界,使这些点都被限制在这个立方体内。 -
在创建
colors
数组存储了与每个顶点对应的 RGB 颜色值,进一步通过LineBasicMaterial( { vertexColors: true } )
设置让每个顶点都带有顶点着色,从而产生五颜六色的渐变效果。Three.js 会从几何体的 “color” 属性里读取每个顶点的颜色,并在相邻顶点之间进行插值(线性过渡)。这样做的效果就是:
- 如果相邻的两个顶点颜色不同,那么在它们之间连接的线段上会逐渐从第一个顶点的颜色过渡到第二个顶点的颜色。
- 当大量顶点的颜色各不相同时,看起来就会形成五颜六色、渐变或杂色的线。
-
最后设置
geometry.setAttribute('position', ...)
和geometry.setAttribute('color', ...)
来将顶点和颜色数据绑定到BufferGeometry
中。
随机颜色的生成思路是:把点的坐标 x / r + 0.5
转换到 [0, 1] 范围内,让每个坐标在空间位置不同时呈现不同的颜色,从而得到多彩的效果。
// 创建一个 BufferGeometry 来容纳顶点和颜色数据
const geometry = new THREE.BufferGeometry();
// 使用最基础的线材质,并启用顶点颜色
const material = new THREE.LineBasicMaterial({ vertexColors: true });
const positions = [];
const colors = [];
// 生成随机顶点及其对应的颜色
for (let i = 0; i < segments; i++) {
const x = Math.random() * r - r / 2; // 在 -r/2 到 r/2 之间取随机数
const y = Math.random() * r - r / 2;
const z = Math.random() * r - r / 2;
// 记录顶点坐标
positions.push(x, y, z);
// 基于位置计算颜色,将坐标转换到 [0,1] 用作RGB分量
colors.push(x / r + 0.5);
colors.push(y / r + 0.5);
colors.push(z / r + 0.5);
}
// 将处理好的顶点和颜色数据,封装进几何体的属性中
geometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(positions, 3)
);
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
// 计算几何体的包围球,提升渲染性能
geometry.computeBoundingSphere();
// 基于几何体和材质创建一条线对象
line = new THREE.Line(geometry, material);
scene.add(line);
之所以选择 BufferGeometry
而不是更简单的 Geometry
,是因为 Geometry
在 Three.js 里已逐渐被废弃或不再推荐使用。
BufferGeometry
会将数据存储在 TypedArray 中,更加高效且贴近底层的 GPU 渲染方式。当 material
里指定了 vertexColors: true
,每个顶点的颜色会在渲染时插值到相邻顶点,形成自然的过渡,若相邻顶点的颜色截然不同,视觉上就会呈现丰富的渐变色彩。
通过以上方式,我们在一个边长为 r
的立方体范围里随机生成了 10000 个顶点,并赋予了每个顶点独有的颜色,这就实现了五颜六色的散点分布。如果将它们用 Line
连接起来,就会像一条闪亮的“彩色线串”,包裹在一个“看不见的立方体”之中。
线是如何按照一定规律进行动画
在下面的示例中,我们并没有做复杂的角色或面部动画,而是用到最简单的「位置信息形变」:在原始几何体和目标几何体之间,对每个顶点的坐标进行插值,从而让线条在两组随机坐标分布之间做周期性往返变化。
上面我们生成了一条包含 10000 个顶点的线,但是要让它在场景中动起来,就需要写动画循环。Three.js 提供了 requestAnimationFrame
或 setAnimationLoop
来不断调用一个名为 animate
的函数。
const delta = clock.getDelta();
获取两帧之间的时间差,用于做基于时间的运动计算,保证动画速率与帧速率解耦。const time = clock.getElapsedTime();
获取从场景开始运行到当前时刻的总时间,可以用来做平滑旋转或其他基于时间的效果。line.rotation.x = time * 0.25; line.rotation.y = time * 0.5;
就是让线沿 X 轴和 Y 轴进行匀速旋转,从而产生一种不断旋转的视觉效果。t += delta * 0.5; line.morphTargetInfluences[ 0 ] = Math.abs( Math.sin( t ) );
这一行与后面会介绍的morphAttributes
关联,用来在顶点之间做插值,从而让线条的位置在两个形态之间来回变化。renderer.render(scene, camera);
负责执行本帧的实际渲染工作,让以上状态变化能呈现在屏幕上。
function animate() {
// 计算帧间隔时间和运行总时间
const delta = clock.getDelta();
const time = clock.getElapsedTime();
// 让线条做持续旋转,提升视觉动感
line.rotation.x = time * 0.25;
line.rotation.y = time * 0.5;
// 利用t来控制morphTargetInfluences,做顶点插值
t += delta * 0.5;
line.morphTargetInfluences[0] = Math.abs(Math.sin(t));
// 渲染当前场景和相机
renderer.render(scene, camera);
// 更新性能监控
stats.update();
}
线是如何包裹在立方体内( morphTargets 的使用)
在 Three.js 中,如果你希望让同一个几何体在不同形态之间做平滑过渡,就需要使用到「形变目标 (Morph Targets)」相关的特性。它主要包含以下关键点:
morphAttributes
:这是附加在BufferGeometry
上的一组属性,用来存储各种形变目标(通常是若干组顶点位置或法线等数据)。morphTargetInfluences
:这是在Mesh
或Line
等可渲染对象上用来控制形变插值的数组。如果你在morphAttributes.position
中放了 N 组“目标形态”,那么就会有morphTargetInfluences.length = N
个数值来控制每个目标的插值程度。- 插值机制:当
morphTargetInfluences[i] = v
( 0 ≤ v ≤ 1 ) 时,渲染时会按照v
的比例将几何体的原始顶点位置插值到目标顶点位置。多个形变目标也可以叠加插值,从而实现复杂的形变动画。
从上面的随机坐标生成逻辑可以看出,每个顶点的坐标 (x, y, z)
都是在 -r/2
到 r/2
的范围内,这本质上定义了一个边长为 r
的立方体。由于线条全部分布在这个区间里,所以从外面看去,就像一条颜色丰富的线被“囚禁”在看不见的立方体之中。
另外,这个示例中还引入了 geometry.morphAttributes.position
,用于给同一个几何体赋予另一组“目标顶点”,并通过在 animate
中设置 line.morphTargetInfluences[0]
的方式来做顶点间的动态插值。下面的代码展示了如何生成这一组额外的顶点数据:
function generateMorphTargets(geometry) {
const data = [];
// 为了与原来的segments数量对应,这里也要同样数量的随机点
for (let i = 0; i < segments; i++) {
const x = Math.random() * r - r / 2;
const y = Math.random() * r - r / 2;
const z = Math.random() * r - r / 2;
data.push(x, y, z);
}
// 创建 morphTarget 属性
const morphTarget = new THREE.Float32BufferAttribute(data, 3);
morphTarget.name = "target1";
// 将这份额外的顶点数据挂载到 geometry 上
geometry.morphAttributes.position = [morphTarget];
}
-
通过
morphAttributes.position
,我们就能在同一个网格上存储多个顶点数据集。 -
在渲染时,可以指定
line.morphTargetInfluences[0]
在[0,1]
区间内插值,0 表示完全使用原始顶点数据,1 表示完全使用目标数据,中间值则会在两者之间进行线性插值。在
animate
函数里,用Math.sin(t)
不断变化该插值因子,就会看到线条的顶点在两个形态间来回切换,让线像在立方体里“呼吸”或“抖动”。 -
由于所有坐标都在
[-r/2, r/2]
的立方体范围内,即使在两个形态间插值,顶点依旧不会超出这个立方体的可视范围,始终“包裹”在我们想要的立方体区域内。
至此,我们就能理解:这条线之所以能被“收纳”在立方体内,是因为它所有的坐标都经过了随机分布在 -r/2
~ r/2
这个区间。 而通过 morphTargets 的插值,我们又可以让这些线条在不同形态之间变化,产生更丰富的动态效果。
总结
-
生成五颜六色线的思路
- 通过
BufferGeometry
+LineBasicMaterial
的组合,将大量随机坐标点连成一条线。 - 每个顶点的颜色值由坐标映射到 [0, 1],并使用
vertexColors: true
来启用顶点着色插值,使整条线呈现出五彩斑斓的渐变。
- 通过
-
按照一定规律进行动画的原理
- 在循环渲染函数
animate
中,用clock.getElapsedTime()
来给线添加旋转,使其绕 X、Y 轴恒定速度旋转。 - 利用
line.morphTargetInfluences
和Math.sin(t)
实现顶点在两组位置数据之间的平滑插值,让线条形态呈周期性呼吸或颤动。
- 在循环渲染函数
-
线是如何包裹在立方体内
- 关键在于顶点的初始和目标位置都被限制在
[-r/2, r/2]
的范围内,所以所有线段都处于一个立方体边界之中。 - 不论是初始顶点数据还是 morph target 数据,都没有越过这个范围,因此从外部视角来看,这些彩色线段就被“困”在一个隐形的立方体里。
- 关键在于顶点的初始和目标位置都被限制在