GPU particle contacts — engine18 Make solid GPU particles collide with one another. await enableGPUParticlePhysics("grains", .12, { mass: .01, gravity: {x:0,y:-9.81,z:0}, bounce:.1, friction:.4, substeps:8 }); await pauseGPUPhysics("grains"); await setGPUParticleContacts("grains", { lower:{x:-8,y:-2,z:-8}, upper:{x:8,y:14,z:8}, cellSize:.5, iterations:3 }); await showGPUParticles("grains", {roughness:.65, castShadows:true}); await startGPUPhysics("grains"); First createGPUProgram with matching initialized positions/velocities xyz buffers. The physics solver owns those buffers; don't also run WGSL that overwrites them. showGPUParticles draws solid lit spheres from the physics buffers entirely on GPU; its visual radius follows the physical radius. Use it instead of showGPUFluid for solid particles. Original GPU visual collections and galaxy programs are unchanged. Give selected particles different sizes, masses or collision groups. await setGPUParticleProfiles("grains", [ {index:0, radius:.12, mass:.01, group:0, mask:65535}, {index:1, radius:.18, mass:.04, group:1, mask:65535} ]); Each entry is complete, not a partial patch. Defaults: radius.1, mass.01, group0, mask65535. Radius .005..2; mass .01..1000. All entries validate before applying. Do setup while paused. showGPUParticles({colorByGroup:true}) uses blue for even and orange for odd groups. tint, roughness, metallic, shadows and LOD controls are also accepted. No Rigidbody is created per particle. Control which particles can collide. Group is0..15. Mask is a16-bit bitset: group0 uses bit1, group1 bit2, etc. Both masks must permit the other group. Mask65535 permits every group;0 none. await excludeGPUParticlePairs("grains", [{a:0,b:1},{a:2,b:3}]); await excludeGPUParticlePairs("grains", []); Up to1024 pairs; each call replaces exclusions. These filters affect only particle pairs within this one program. They do not filter ordinary CS colliders or create collisions between separate GPU programs. Switch self-contact off to compare particles passing through one another. await setGPUParticleContacts("grains", {enabled:false}); await setGPUParticleContacts("grains", {enabled:true}); Settings patch existing contact settings. Grid allocation size cannot change once created: recreate the program for a differently sized grid. Cell size must be at least twice the largest radius. Max1048576 cells,128 candidates per cell traversal, 8192 particles. Bounds are rounded outward to cell boundaries. They describe the neighbor search domain, not walls. Add GPU obstacles/colliders to contain particles. A32-byte asynchronous health sample pauses on out-of-grid particles or traversal overflow with an error; enlarge/reposition bounds or spread particles, then reset. Never silently treat overflow as successful collision solving. Load a deformable sheet with GPU particles. await createGPUMesh("sheet","grid", { origin:{x:0,y:2,z:0}, size:{x:6,y:1,z:6}, resolution:16 }); await enableGPUMeshPhysics("sheet", { mass:.25, stiffness:100, foundation:1, damping:2, pinBoundary:true }); await showGPUPhysicsEnergy("sheet", .03); await attachGPUDeformable("grains", "sheet"); Particles and mesh exchange barycentric impulses on GPU; pinned edges support the load. The particle solver advances the sheet while attached, so pausing particles pauses the coupled simulation. One particle program per sheet. The sheet must be running before attachment. Detach with detachGPUDeformable("grains"); removing either program detaches safely. Pause particles before resetting both programs. Limit2097152 particle-face and particle-vertex tests per substep. This bounded triangle query is intended for small demonstrations, not huge cloth scenes. The current sheet is an oriented, linear-elastic spring network; not nonlinear cloth, tearing, bending-shell FEM, VBD or a validated engineering stress solver. Its color is elastic energy, not stress in pascals. Interact with ordinary CS physics objects using the existing bridge. await addGPUCollider("grains", "box1"); Box1 must already be loaded and have its normal collider/Rigidbody setup. Sphere, box and capsule colliders are supported. Dynamic bodies receive opposite total impulse and torque through a small asynchronous readback. No per-particle position readback. Keep total GPU particle mass <= one-quarter of each dynamic body's mass. Mass, constraints and interpolation of CS objects are never altered. Existing addGPUSceneCollider supports static mesh/terrain collision snapshots. Inspect contact quality and cost. var result = await getGPUParticleState("grains"); var stats = result.particleDiagnostics; Read once/second: sampled/pending, particles/cells, pairs/candidateTests, maxOccupancy/maxOverlap, outside/overflow, substepTravelRadii, dispatchMilliseconds. First sample can be pending. Values describe the last sampled configuration. Overlap is world distance; pairs are overlapping candidates, not all historical impacts. dispatchMilliseconds measures CPU command submission, not GPU execution. substepTravelRadii much above1 risks tunneling: reduce speed or increase substeps. Self-contact is discrete sphere contact with iterative Jacobi position correction, friction and restitution. It is not continuous collision detection or a fluid solver. Existing analytic scenery sweeps still apply to ordinary obstacle collisions. Restore/share from editor code: create buffers, enable physics, profiles/filters, contact settings, obstacles, optional sheet connection, visual settings and UI. resetGPUPhysics restores initial positions/velocities while keeping profiles and contact configuration. clearRun removes programs and releases GPU allocations. Check getGPUCapabilities().gpuParticleContacts / gpuParticleProfiles / gpuDeformableCoupling; refresh Game for a stale player, not just Run. Four recipes: catalog/examples/gpu-contacts-pile.js, gpu-contacts-masses.js, gpu-contacts-bed.js, gpu-contacts-cloth.js. Each has Reset, Pause, camera views, Inspect contacts and a real browser unequal-mass rebound verification button.