细胞碰撞实验:使用Rapier与Three.js打造有机粒子系统

本文详细介绍了使用Rapier物理引擎和Three.js创建有机细胞粒子实验的技术实现,包括物理碰撞检测、性能优化策略、后处理效果应用以及实时交互的实现方法。

细胞碰撞实验:使用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
}
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计