GPU scenery collisions and gameplay contact events — engine v16 Requires getGPUCapabilities().gpuSceneryCollision and gpuContactEvents. Extends GPU_PHYSICS.txt; existing sphere/box methods and ordinary CS physics remain. Register a loaded object's sphere, box or capsule collider with a GPU solver. addGPUCollider("particles", "objectname") Capsules: analytic particle shape, exact segment sweep for stationary axis. Elastic sheets approximate a capsule by up to32 spheres along its axis (64 total samples/program); this is not exact continuous capsule/triangle collision. Sheared collider transforms are rejected. Rigidbody mass, constraints, interpolation and CS physics scripts are preserved. Static/kinematic shapes are external energy sources; dynamic shapes receive opposite impulses/torque. Kinematic translation/angular velocity comes from transform changes between GPU steps. Sweeps account for translation; rapidly rotating obstacles are not exact CCD. Let GPU particles collide with existing terrain or static mesh scenery. addGPUSceneCollider("particles", "terrainName") addGPUSceneCollider("particles", "buildingName") Requires enabled nontrigger TerrainCollider or readable MeshCollider, no attached Rigidbody. Snapshots contain world-space triangles plus a stackless BVH. Maximum 65536 triangles per collider,16 total colliders/program,128MiB total program budget. Terrain native heightmap resolution must fit the triangle limit (Unity129 fits; 257 does not). Holes are omitted. No silent downsampling. Degenerate faces skipped. Scene geometry is not drawn or duplicated by registration. Keep the original visible object and its PhysX collider. Ordinary CS bodies already collide with it. Static snapshots currently collide with GPU particles, not deforming GPU meshes. Refresh after changing scenery vertices or sculpting/painting holes in terrain. refreshGPUSceneCollider("particles", "terrainName") Positions/matrices and replaced mesh/terrain resources are checked; changed static transforms pause the solver with a readable error until refreshed and restarted. In-place terrain/mesh edits require explicit refresh; there is no per-frame hash. Refresh preserves previous snapshot on error. It may require temporary memory for both old/new snapshots. Use startGPUPhysics after a transform-change pause. Collision scope: double-sided static triangle faces, discrete edges/corners, swept faces preventing fast normal crossings. Not full swept sphere-edge CCD, arbitrary mesh-mesh dynamics or particle self-collision. GPU geometry remains resident; only bounded reaction summaries cross to CPU. Scene BVH prunes candidates. getGPUPhysicsState adds sceneryTriangles,sceneryNodes,triangleTests (last readback batch across substeps),eventQueue,eventOverflow. Timing is asynchronous readback latency, not compute-kernel duration. State/scene bounds are world coordinates; legacy CS object.pos.x uses the established opposite-X convention. Get retained enter/exit notifications for sounds, UI or scoring. setGPUContactEvents("particles", {other:"buildingName", minimumImpulse:0.01}) var reply = await getGPUContactEvents("particles") for (var hit of reply.events) { if (hit.phase === "enter") console.log(hit.other, hit.point, hit.impulse) } Defaults: all names, minimumImpulse0, stayfalse. Option stay:true includes one aggregate stay per collider per processed contact batch. other is an exact name. Impulse threshold applies when admitting a contact episode, then exit is retained. Events aggregate all particle/sample contacts for a collider. They do not identify individual particles. Static particle hits report averaged point and total reaction impulse, no unique normal/triangle; elastic mesh contacts can retain triangle data. Fields: sequence,step,other,collider,key,phase,point,normal,impulse,triangle, barycentric,separation,age. Sequence increases within solver lifetime. Queue holds 256 events, drops oldest on overflow and increments eventOverflow. Reads drain up to64 by default (argument1..256). Use one consumer; route results to UI/audio. Polling may use a normal CS managed interval; NEVER implement forces with JS loops. Reset/pause/registration changes clear pending events and active episodes; the next contact begins a fresh enter. Remove/dispose clears callbacks/readbacks. Existing collisionWatch is unchanged and still handles ordinary PhysX collisions. For a new drop, pause before resetting or changing collider registration, then start once. Otherwise the running solver can impact during the awaited reset and the following start clears that event. Use this sequence: await pauseGPUPhysics("particles") await resetGPUPhysics("particles") await startGPUPhysics("particles") Ten separate GPU Scenery demo activities show capsule, terrain, mesh, refresh, BVH pruning, moving obstacles, events, filters, contact markers and lifecycle. Examples live in catalog/examples/gpu-scenery-*.js after deployment.