细胞碰撞实验:使用Rapier与Three.js打造有机粒子系统
概念起源
每个项目都始于好奇的火花。这个项目的核心灵感来自于探索粒子模拟技术,特别是观看使用Cinema 4D的xParticles插件创建细胞状粒子的教程后。团队经常从3D动态设计技术中汲取灵感,工作室里经常出现这样的问题:“如果这能变成交互式的,不是很酷吗?“这就是创意的诞生。
基于示例在C4D中建立自己的设置后,我们创建了一个通用运动原型来演示交互。结果产生了一种排斥效果,细胞根据光标位置发生位移。为了创建演示,我们添加了一个简单的球体并赋予其碰撞器标签,这样当球体在模拟中移动时,粒子会被推开,模拟鼠标移动。
艺术指导
确定基础粒子和交互演示后,我们渲染出序列并在After Effects中开始调整视觉效果。我们希望通过叠加几个效果来实现:
- Effect > Generate > 4 Colour Gradient:添加到新的形状图层
- Effect > Blur > Camera Blur:添加到新的调整图层
- Effect > Blur > Compound Blur:添加到同一调整图层
- Effect > Color Correction > Colorama:作为新的调整图层添加
技术方法与工具
由于这是一个简单的单页静态站点,不需要后端,我们使用了内部基于Astro与Vite和Three.js的样板。对于物理效果,我们选择了Rapier,因为它能高效处理碰撞检测并且与Three.js兼容。
物理实现
我们需要在3D渲染世界旁边创建一个独立的物理世界,因为Rapier库只处理物理计算,图形部分由开发者选择实现。
以下是创建刚体的代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
for (let i = 0; i < this.numberOfBodies; i++) {
const x = Math.random() * this.bounds.x - this.bounds.x * 0.5
const y = Math.random() * this.bounds.y - this.bounds.y * 0.5
const z = Math.random() * (this.bounds.z * 0.95) - (this.bounds.z * 0.95) * 0.5
const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y, z)
bodyDesc.setGravityScale(0.0) // 禁用重力
bodyDesc.setLinearDamping(0.7)
const body = this.physicsWorld.createRigidBody(bodyDesc)
const radius = MathUtils.mapLinear(Math.random(), 0.0, 1.0, this._cellSizeRange[0], this._cellSizeRange[1])
const colliderDesc = RAPIER.ColliderDesc.ball(radius)
const collider = this.physicsWorld.createCollider(colliderDesc, body)
collider.setRestitution(0.1) // 弹性 0=无弹跳,1=完全弹跳
this.bodies.push(body)
this.colliders.push(collider)
}
|
网格单独创建,每个tick时,它们的变换通过物理引擎更新:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 更新网格位置
for (let i = 0; i < this.numberOfBodies; i++) {
const body = this.bodies[i]
const position = body.translation()
const collider = this.colliders[i]
const radius = collider.shape.radius
this._dummy.position.set(position.x, position.y, position.z)
this._dummy.scale.setScalar(radius)
this._dummy.updateMatrix()
this.mesh.setMatrixAt(i, this._dummy.matrix)
}
this.mesh.instanceMatrix.needsUpdate = true
|
后处理构建视觉效果
后处理效果在这个项目中扮演重要角色。最重要的是模糊效果,它使细胞从清晰的简单环转变为流动的粘稠物质。我们实现了Kawase模糊,它类似于高斯模糊,但使用盒式模糊而不是高斯函数,在较高模糊级别下性能更好。
色彩分级也是体验的重要部分,我们将颜色映射到场景中元素的亮度:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
if (uFluidEnabled) {
fluidColor = texture2D(tFluid, screenCoords);
fluid = pow(luminance(abs(fluidColor.rgb)), 1.2);
fluid *= 0.28;
}
vec3 color1 = uColor1 - fluid * 0.08;
vec3 color2 = uColor2 - fluid * 0.08;
vec3 color3 = uColor3 - fluid * 0.08;
vec3 color4 = uColor4 - fluid * 0.08;
if (uEnabled) {
// 应用色彩分级
color = getColorRampColor(brightness, uStops.x, uStops.y, uStops.z, uStops.w, color1, color2, color3, color4);
}
color += color * fluid * 1.5;
color = clamp(color, 0.0, 1.0);
color += color * fluidColor.rgb * 0.09;
gl_FragColor = vec4(color, 1.0);
|
性能优化
随着物理引擎所需计算能力的快速增加,我们力求使体验尽可能优化。第一步是找到不影响视觉效果的最小细胞数量。为此,我们最小化了细胞创建的区域并使细胞略大。
另一个重要步骤是确保没有冗余计算,这意味着每个计算都必须通过屏幕上可见的结果来证明是合理的:
1
2
3
4
5
6
7
8
9
10
|
this.camera._width = this.camera.right - this.camera.left
this.camera._height = this.camera.top - this.camera.bottom
// .....
this.bounds = {
x: (this.camera._width / this.options.cameraZoom) * 0.5,
y: (this.camera._height / this.options.cameraZoom) * 0.5,
z: 0.5
}
|