Particle Physics
Apply forces, fields, and constraints to create dynamic particle motion.
Quick Start
tsx1// 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)
tsx1function 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)
tsx1function 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)
tsx1function 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
tsx1function 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
tsx1function 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
tsx1interface 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
tsx1// 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)
tsx1class 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
tsx1function 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
glsl1// 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
tsx1function 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
tsx1function 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
tsx1function 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)
tsx1// Fastest, least accurate 2position += velocity * delta; 3velocity += acceleration * delta;
Verlet (Better for constraints)
tsx1// Store previous position 2const newPos = position * 2 - prevPosition + acceleration * delta * delta; 3prevPosition = position; 4position = newPos;
RK4 (Most accurate)
tsx1// 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 implementationsreferences/integration.md— When to use which integration method