Particle Physics
Apply forces, fields, and constraints to create dynamic particle motion.
Quick Start
tsx
1// Simple gravity + velocity
2useFrame((_, delta) => {
3 for (let i = 0; i < count; i++) {
4 // Apply gravity
5 velocities[i * 3 + 1] -= 9.8 * delta;
6
7 // Update position
8 positions[i * 3] += velocities[i * 3] * delta;
9 positions[i * 3 + 1] += velocities[i * 3 + 1] * delta;
10 positions[i * 3 + 2] += velocities[i * 3 + 2] * delta;
11 }
12 geometry.attributes.position.needsUpdate = true;
13});
Force Types
Gravity (Constant Force)
tsx
1function applyGravity(
2 velocities: Float32Array,
3 count: number,
4 gravity: THREE.Vector3,
5 delta: number,
6) {
7 for (let i = 0; i < count; i++) {
8 velocities[i * 3] += gravity.x * delta;
9 velocities[i * 3 + 1] += gravity.y * delta;
10 velocities[i * 3 + 2] += gravity.z * delta;
11 }
12}
13
14// Usage
15const gravity = new THREE.Vector3(0, -9.8, 0);
16applyGravity(velocities, count, gravity, delta);
Wind (Directional + Noise)
tsx
1function applyWind(
2 velocities: Float32Array,
3 positions: Float32Array,
4 count: number,
5 direction: THREE.Vector3,
6 strength: number,
7 turbulence: number,
8 time: number,
9 delta: number,
10) {
11 for (let i = 0; i < count; i++) {
12 const x = positions[i * 3];
13 const y = positions[i * 3 + 1];
14 const z = positions[i * 3 + 2];
15
16 // Base wind
17 let wx = direction.x * strength;
18 let wy = direction.y * strength;
19 let wz = direction.z * strength;
20
21 // Add turbulence (using simple noise approximation)
22 const noise = Math.sin(x * 0.5 + time) * Math.cos(z * 0.5 + time);
23 wx += noise * turbulence;
24 wy += Math.sin(y * 0.3 + time * 1.3) * turbulence * 0.5;
25 wz += Math.cos(x * 0.4 + time * 0.7) * turbulence;
26
27 velocities[i * 3] += wx * delta;
28 velocities[i * 3 + 1] += wy * delta;
29 velocities[i * 3 + 2] += wz * delta;
30 }
31}
Drag (Velocity Damping)
tsx
1function applyDrag(
2 velocities: Float32Array,
3 count: number,
4 drag: number, // 0-1, higher = more drag
5 delta: number,
6) {
7 const factor = 1 - drag * delta;
8
9 for (let i = 0; i < count; i++) {
10 velocities[i * 3] *= factor;
11 velocities[i * 3 + 1] *= factor;
12 velocities[i * 3 + 2] *= factor;
13 }
14}
15
16// Quadratic drag (more realistic)
17function applyQuadraticDrag(
18 velocities: Float32Array,
19 count: number,
20 coefficient: number,
21 delta: number,
22) {
23 for (let i = 0; i < count; i++) {
24 const vx = velocities[i * 3];
25 const vy = velocities[i * 3 + 1];
26 const vz = velocities[i * 3 + 2];
27
28 const speed = Math.sqrt(vx * vx + vy * vy + vz * vz);
29 if (speed > 0) {
30 const dragForce = coefficient * speed * speed;
31 const factor = Math.max(0, 1 - (dragForce * delta) / speed);
32
33 velocities[i * 3] *= factor;
34 velocities[i * 3 + 1] *= factor;
35 velocities[i * 3 + 2] *= factor;
36 }
37 }
38}
Attractors & Repulsors
Point Attractor
tsx
1function applyAttractor(
2 velocities: Float32Array,
3 positions: Float32Array,
4 count: number,
5 attractorPos: THREE.Vector3,
6 strength: number, // Positive = attract, negative = repel
7 delta: number,
8) {
9 for (let i = 0; i < count; i++) {
10 const dx = attractorPos.x - positions[i * 3];
11 const dy = attractorPos.y - positions[i * 3 + 1];
12 const dz = attractorPos.z - positions[i * 3 + 2];
13
14 const distSq = dx * dx + dy * dy + dz * dz;
15 const dist = Math.sqrt(distSq);
16
17 if (dist > 0.1) {
18 // Avoid division by zero
19 // Inverse square falloff
20 const force = strength / distSq;
21
22 velocities[i * 3] += (dx / dist) * force * delta;
23 velocities[i * 3 + 1] += (dy / dist) * force * delta;
24 velocities[i * 3 + 2] += (dz / dist) * force * delta;
25 }
26 }
27}
Orbit Attractor
tsx
1function applyOrbitAttractor(
2 velocities: Float32Array,
3 positions: Float32Array,
4 count: number,
5 center: THREE.Vector3,
6 orbitStrength: number,
7 pullStrength: number,
8 delta: number,
9) {
10 for (let i = 0; i < count; i++) {
11 const dx = positions[i * 3] - center.x;
12 const dy = positions[i * 3 + 1] - center.y;
13 const dz = positions[i * 3 + 2] - center.z;
14
15 const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
16
17 if (dist > 0.1) {
18 // Tangential force (orbit)
19 const tx = -dz / dist;
20 const tz = dx / dist;
21
22 velocities[i * 3] += tx * orbitStrength * delta;
23 velocities[i * 3 + 2] += tz * orbitStrength * delta;
24
25 // Radial force (pull toward center)
26 velocities[i * 3] -= (dx / dist) * pullStrength * delta;
27 velocities[i * 3 + 1] -= (dy / dist) * pullStrength * delta;
28 velocities[i * 3 + 2] -= (dz / dist) * pullStrength * delta;
29 }
30 }
31}
Multiple Attractors
tsx
1interface Attractor {
2 position: THREE.Vector3;
3 strength: number;
4 radius: number; // Influence radius
5}
6
7function applyAttractors(
8 velocities: Float32Array,
9 positions: Float32Array,
10 count: number,
11 attractors: Attractor[],
12 delta: number,
13) {
14 for (let i = 0; i < count; i++) {
15 const px = positions[i * 3];
16 const py = positions[i * 3 + 1];
17 const pz = positions[i * 3 + 2];
18
19 for (const attractor of attractors) {
20 const dx = attractor.position.x - px;
21 const dy = attractor.position.y - py;
22 const dz = attractor.position.z - pz;
23
24 const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
25
26 if (dist > 0.1 && dist < attractor.radius) {
27 // Smooth falloff within radius
28 const falloff = 1 - dist / attractor.radius;
29 const force = attractor.strength * falloff * falloff;
30
31 velocities[i * 3] += (dx / dist) * force * delta;
32 velocities[i * 3 + 1] += (dy / dist) * force * delta;
33 velocities[i * 3 + 2] += (dz / dist) * force * delta;
34 }
35 }
36 }
37}
Velocity Fields
Curl Noise Field
tsx
1// In shader (GPU)
2vec3 curlNoise(vec3 p) {
3 const float e = 0.1;
4
5 vec3 dx = vec3(e, 0.0, 0.0);
6 vec3 dy = vec3(0.0, e, 0.0);
7 vec3 dz = vec3(0.0, 0.0, e);
8
9 float n1 = snoise(p + dy) - snoise(p - dy);
10 float n2 = snoise(p + dz) - snoise(p - dz);
11 float n3 = snoise(p + dx) - snoise(p - dx);
12 float n4 = snoise(p + dz) - snoise(p - dz);
13 float n5 = snoise(p + dx) - snoise(p - dx);
14 float n6 = snoise(p + dy) - snoise(p - dy);
15
16 return normalize(vec3(n1 - n2, n3 - n4, n5 - n6));
17}
18
19// Usage in vertex shader
20vec3 velocity = curlNoise(position * 0.5 + uTime * 0.1);
21position += velocity * delta;
Flow Field (2D/3D Grid)
tsx
1class FlowField {
2 private field: THREE.Vector3[];
3 private resolution: number;
4 private size: number;
5
6 constructor(resolution: number, size: number) {
7 this.resolution = resolution;
8 this.size = size;
9 this.field = [];
10
11 for (let i = 0; i < resolution ** 3; i++) {
12 this.field.push(new THREE.Vector3());
13 }
14 }
15
16 // Generate field from noise
17 generate(time: number, scale: number) {
18 for (let x = 0; x < this.resolution; x++) {
19 for (let y = 0; y < this.resolution; y++) {
20 for (let z = 0; z < this.resolution; z++) {
21 const index =
22 x + y * this.resolution + z * this.resolution * this.resolution;
23
24 // Use noise to generate flow direction
25 const wx = (x / this.resolution) * scale;
26 const wy = (y / this.resolution) * scale;
27 const wz = (z / this.resolution) * scale;
28
29 const angle1 = noise3D(wx, wy, wz + time) * Math.PI * 2;
30 const angle2 = noise3D(wx + 100, wy, wz + time) * Math.PI * 2;
31
32 this.field[index].set(
33 Math.cos(angle1) * Math.cos(angle2),
34 Math.sin(angle2),
35 Math.sin(angle1) * Math.cos(angle2),
36 );
37 }
38 }
39 }
40 }
41
42 // Sample field at position
43 sample(position: THREE.Vector3): THREE.Vector3 {
44 const halfSize = this.size / 2;
45
46 const x = Math.floor(
47 ((position.x + halfSize) / this.size) * this.resolution,
48 );
49 const y = Math.floor(
50 ((position.y + halfSize) / this.size) * this.resolution,
51 );
52 const z = Math.floor(
53 ((position.z + halfSize) / this.size) * this.resolution,
54 );
55
56 const cx = Math.max(0, Math.min(this.resolution - 1, x));
57 const cy = Math.max(0, Math.min(this.resolution - 1, y));
58 const cz = Math.max(0, Math.min(this.resolution - 1, z));
59
60 const index =
61 cx + cy * this.resolution + cz * this.resolution * this.resolution;
62 return this.field[index];
63 }
64}
Vortex Field
tsx
1function applyVortex(
2 velocities: Float32Array,
3 positions: Float32Array,
4 count: number,
5 center: THREE.Vector3,
6 axis: THREE.Vector3, // Normalized
7 strength: number,
8 falloff: number,
9 delta: number,
10) {
11 for (let i = 0; i < count; i++) {
12 const dx = positions[i * 3] - center.x;
13 const dy = positions[i * 3 + 1] - center.y;
14 const dz = positions[i * 3 + 2] - center.z;
15
16 // Project onto plane perpendicular to axis
17 const dot = dx * axis.x + dy * axis.y + dz * axis.z;
18 const px = dx - dot * axis.x;
19 const py = dy - dot * axis.y;
20 const pz = dz - dot * axis.z;
21
22 const dist = Math.sqrt(px * px + py * py + pz * pz);
23
24 if (dist > 0.1) {
25 // Tangent direction (cross product with axis)
26 const tx = axis.y * pz - axis.z * py;
27 const ty = axis.z * px - axis.x * pz;
28 const tz = axis.x * py - axis.y * px;
29
30 const tLen = Math.sqrt(tx * tx + ty * ty + tz * tz);
31 const force = strength * Math.exp(-dist * falloff);
32
33 velocities[i * 3] += (tx / tLen) * force * delta;
34 velocities[i * 3 + 1] += (ty / tLen) * force * delta;
35 velocities[i * 3 + 2] += (tz / tLen) * force * delta;
36 }
37 }
38}
Turbulence
Simplex-Based Turbulence
glsl
1// GPU turbulence in vertex shader
2vec3 turbulence(vec3 p, float time, float scale, int octaves) {
3 vec3 result = vec3(0.0);
4 float amplitude = 1.0;
5 float frequency = scale;
6
7 for (int i = 0; i < octaves; i++) {
8 vec3 samplePos = p * frequency + time;
9 result.x += snoise(samplePos) * amplitude;
10 result.y += snoise(samplePos + vec3(100.0)) * amplitude;
11 result.z += snoise(samplePos + vec3(200.0)) * amplitude;
12
13 frequency *= 2.0;
14 amplitude *= 0.5;
15 }
16
17 return result;
18}
CPU Turbulence
tsx
1function applyTurbulence(
2 velocities: Float32Array,
3 positions: Float32Array,
4 count: number,
5 strength: number,
6 scale: number,
7 time: number,
8 delta: number,
9) {
10 for (let i = 0; i < count; i++) {
11 const x = positions[i * 3] * scale;
12 const y = positions[i * 3 + 1] * scale;
13 const z = positions[i * 3 + 2] * scale;
14
15 // Simple noise approximation
16 const nx = Math.sin(x + time) * Math.cos(z + time * 0.7);
17 const ny = Math.sin(y + time * 1.3) * Math.cos(x + time * 0.5);
18 const nz = Math.sin(z + time * 0.9) * Math.cos(y + time * 1.1);
19
20 velocities[i * 3] += nx * strength * delta;
21 velocities[i * 3 + 1] += ny * strength * delta;
22 velocities[i * 3 + 2] += nz * strength * delta;
23 }
24}
Collision
Plane Collision
tsx
1function collidePlane(
2 positions: Float32Array,
3 velocities: Float32Array,
4 count: number,
5 planeY: number,
6 bounce: number, // 0-1
7) {
8 for (let i = 0; i < count; i++) {
9 if (positions[i * 3 + 1] < planeY) {
10 positions[i * 3 + 1] = planeY;
11 velocities[i * 3 + 1] *= -bounce;
12 }
13 }
14}
Sphere Collision
tsx
1function collideSphere(
2 positions: Float32Array,
3 velocities: Float32Array,
4 count: number,
5 center: THREE.Vector3,
6 radius: number,
7 bounce: number,
8 inside: boolean, // true = contain inside, false = repel from outside
9) {
10 for (let i = 0; i < count; i++) {
11 const dx = positions[i * 3] - center.x;
12 const dy = positions[i * 3 + 1] - center.y;
13 const dz = positions[i * 3 + 2] - center.z;
14
15 const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
16
17 const collision = inside ? dist > radius : dist < radius;
18
19 if (collision && dist > 0) {
20 const nx = dx / dist;
21 const ny = dy / dist;
22 const nz = dz / dist;
23
24 // Move to surface
25 const targetDist = inside ? radius : radius;
26 positions[i * 3] = center.x + nx * targetDist;
27 positions[i * 3 + 1] = center.y + ny * targetDist;
28 positions[i * 3 + 2] = center.z + nz * targetDist;
29
30 // Reflect velocity
31 const dot =
32 velocities[i * 3] * nx +
33 velocities[i * 3 + 1] * ny +
34 velocities[i * 3 + 2] * nz;
35 velocities[i * 3] = (velocities[i * 3] - 2 * dot * nx) * bounce;
36 velocities[i * 3 + 1] = (velocities[i * 3 + 1] - 2 * dot * ny) * bounce;
37 velocities[i * 3 + 2] = (velocities[i * 3 + 2] - 2 * dot * nz) * bounce;
38 }
39 }
40}
Integration Methods
Euler (Simple)
tsx
1// Fastest, least accurate
2position += velocity * delta;
3velocity += acceleration * delta;
Verlet (Better for constraints)
tsx
1// Store previous position
2const newPos = position * 2 - prevPosition + acceleration * delta * delta;
3prevPosition = position;
4position = newPos;
RK4 (Most accurate)
tsx
1// Runge-Kutta 4th order (for high precision)
2function rk4(
3 position: number,
4 velocity: number,
5 acceleration: (p: number, v: number) => number,
6 dt: number,
7) {
8 const k1v = acceleration(position, velocity);
9 const k1x = velocity;
10
11 const k2v = acceleration(
12 position + (k1x * dt) / 2,
13 velocity + (k1v * dt) / 2,
14 );
15 const k2x = velocity + (k1v * dt) / 2;
16
17 const k3v = acceleration(
18 position + (k2x * dt) / 2,
19 velocity + (k2v * dt) / 2,
20 );
21 const k3x = velocity + (k2v * dt) / 2;
22
23 const k4v = acceleration(position + k3x * dt, velocity + k3v * dt);
24 const k4x = velocity + k3v * dt;
25
26 return {
27 position: position + ((k1x + 2 * k2x + 2 * k3x + k4x) * dt) / 6,
28 velocity: velocity + ((k1v + 2 * k2v + 2 * k3v + k4v) * dt) / 6,
29 };
30}
File Structure
particles-physics/
├── SKILL.md
├── references/
│ ├── forces.md # All force types
│ └── integration.md # Integration methods comparison
└── scripts/
├── forces/
│ ├── gravity.ts # Gravity implementations
│ ├── attractors.ts # Point/orbit attractors
│ └── fields.ts # Flow/velocity fields
└── collision/
├── planes.ts # Plane collision
└── shapes.ts # Sphere, box collision
Reference
references/forces.md — Complete force implementations
references/integration.md — When to use which integration method