#version 450
// GPU body-to-body contacts for the built-in ECS physics shader, in two
// passes selected by a define. GRID_PASS copies every body into the
// snapshot and files the bodies that collide into a spatial hash; the
// contact pass then moves each dynamic body out of its neighbours, reading
// only the snapshot, so the result does not depend on invocation order.
// Bodies whose cell is full, and bodies too big for one cell, go to the
// fallback list instead, which every body tests: no body is ever left out.
layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in;
#include <physics_abi.glsl>
#include <physics_shapes.glsl>
// Bodies per hash cell before the rest spill into the fallback list.
const uint CELL_SLOTS = 8u;
// Cell counts; the length is a power of two.
layout(set = 0, binding = 8) buffer GridCounts { uint data[]; } grid_counts;
layout(set = 0, binding = 9) buffer GridSlots { uint data[]; } grid_slots;
// data[0] = fallback count, then the fallback body indices.
layout(set = 0, binding = 10) buffer Fallback { uint data[]; } fallback;
layout(set = 0, binding = 11) buffer Snapshot { PhysicsState data[]; } snapshot;
// Full, Simplified and Space bodies with a solid shape touch each other;
// NoCollision (2) and Custom (3) bodies do not.
bool takes_part(PhysicsState body, BodyShape shape) {
precise float from_no_collision = abs(body.custom_values.x - 2.0);
precise float from_custom = abs(body.custom_values.x - 3.0);
return shape.shape.x < 2.5 && from_no_collision > 0.5 && from_custom > 0.5;
}
float inverse_mass(PhysicsState body) {
bool dynamic = body.properties.z > 0.5 && body.properties.z < 1.5 && body.properties.x > 0.0;
return dynamic ? sim_recip(body.properties.x) : 0.0;
}
ivec3 cell_of(vec3 position) {
precise vec3 scaled = position * sim_recip(pc.grid_cell_size);
return sim_to_int(floor(scaled));
}
uint hash_cell(ivec3 cell) {
uvec3 u = uvec3(cell);
return (u.x * 73856093u ^ u.y * 19349663u ^ u.z * 83492791u) & uint(grid_counts.data.length() - 1);
}
bool oversized(BodyShape shape) {
precise float diameter = bounding_radius(shape.shape) * 2.0;
return diameter > pc.grid_cell_size;
}
#ifdef GRID_PASS
void add_to_fallback(uint body_index) {
uint slot = atomicAdd(fallback.data[0], 1u);
fallback.data[1u + slot] = body_index;
}
void main() {
uint body_index = gl_GlobalInvocationID.x;
if (body_index >= pc.body_count) return;
PhysicsState body = bodies.data[body_index];
snapshot.data[body_index] = body;
BodyShape shape = body_shapes.data[body_index];
if (!takes_part(body, shape)) return;
if (oversized(shape)) {
atomicAdd(event_header.contacts.y, 1u);
add_to_fallback(body_index);
return;
}
uint cell = hash_cell(cell_of(body.model[3].xyz));
uint slot = atomicAdd(grid_counts.data[cell], 1u);
if (slot < CELL_SLOTS) {
grid_slots.data[cell * CELL_SLOTS + slot] = body_index;
} else {
atomicAdd(event_header.contacts.x, 1u);
add_to_fallback(body_index);
}
}
#else
PhysicsState own;
BodyShape own_shape;
mat3 own_rotation;
uint own_index;
float own_inverse_mass;
// Sums in fixed point: integer addition gives the same result in any
// order, and the order differs between runs because the grid and fallback
// lists fill in atomic order.
ivec3 push;
ivec3 velocity_change;
bool touched;
uint hash_collisions;
uint fallback_tests;
// 1/65536 m (or m/s) steps; a contribution is clamped to 256 so 128 of
// them cannot overflow.
const float FIXED_SCALE = 65536.0;
const float FIXED_STEP = 1.0 / FIXED_SCALE;
ivec3 to_fixed(vec3 value) {
// roundEven: GLSL leaves the direction of round() at .5 to the device.
precise vec3 scaled = clamp(value, -256.0, 256.0) * FIXED_SCALE;
return sim_to_int(roundEven(scaled));
}
// Moves `own` out of `other_index` by its share of the overlap and removes
// its share of the closing velocity, like a collider contact without torque.
// ponytail: the overlap comes from own center to the other surface plus own
// reach, exact for spheres and face contacts but loose on edges; add a
// real narrow phase if stacks of rotated boxes need it.
void touch(uint other_index) {
if (other_index == own_index) return;
PhysicsState other = snapshot.data[other_index];
BodyShape other_shape = body_shapes.data[other_index];
if ((own_shape.layers.x & other_shape.layers.y) == 0u || (other_shape.layers.x & own_shape.layers.y) == 0u) return;
precise vec3 offset = own.model[3].xyz - other.model[3].xyz;
precise float reach = bounding_radius(own_shape.shape) + bounding_radius(other_shape.shape);
precise float reach_squared = reach * reach;
if (sim_dot(offset, offset) >= reach_squared) return;
vec3 normal;
float distance;
shape_distance(other_shape.shape, rotation_of(other.model), other.model[3].xyz, own.model[3].xyz, normal, distance);
precise float depth = support(own_shape.shape, own_rotation, normal) - distance;
if (depth <= 0.0) return;
touched = true;
precise float total = own_inverse_mass + inverse_mass(other);
if (total <= 0.0) return;
float share = sim_div(own_inverse_mass, total);
// Shock propagation: a body below that was supported last step (touch
// flag in velocity.w) counts as immovable, and it ignores the body
// resting on it. One pass then holds a tall stack instead of splitting
// each correction in half and letting the weight sink through.
vec3 gravity = vec3(pc.gravity_x, pc.gravity_y, pc.gravity_z);
if (sim_dot(gravity, gravity) > 0.0) {
float up = sim_dot(normal, -sim_normalize(gravity));
if (up > 0.7 && other.velocity.w > 0.5 && own_inverse_mass > 0.0) share = 1.0;
else if (up < -0.7 && own.velocity.w > 0.5) share = 0.0;
}
precise vec3 correction = normal * depth * share;
push += to_fixed(correction);
precise vec3 other_velocity = other.velocity.xyz;
// A supporting body sinks only because this pass lags a step; matching
// that sink would add g * dt of fall per level of a stack.
if (share == 1.0) other_velocity -= normal * min(sim_dot(other_velocity, normal), 0.0);
precise vec3 relative = own.velocity.xyz - other_velocity;
float closing = sim_dot(relative, normal);
if (closing >= 0.0) return;
// Bounce only above 1 m/s so resting contacts settle.
float restitution = closing < -1.0 ? max(own_shape.material.y, other_shape.material.y) : 0.0;
precise vec3 change = -normal * closing * (1.0 + restitution);
precise vec3 tangent = relative - normal * closing;
float tangent_speed = sim_length(tangent);
if (tangent_speed > 0.000001) {
precise float friction_squared = own_shape.material.x * other_shape.material.x;
float friction = sim_sqrt(friction_squared);
change -= sim_div(tangent, tangent_speed) * min(tangent_speed, -friction * closing);
}
precise vec3 shared_change = change * share;
velocity_change += to_fixed(shared_change);
}
void touch_in_cell(uint other_index, ivec3 cell) {
if (cell_of(snapshot.data[other_index].model[3].xyz) != cell) hash_collisions++;
touch(other_index);
}
void touch_fallback(uint other_index) {
fallback_tests++;
touch(other_index);
}
void main() {
own_index = gl_GlobalInvocationID.x;
if (own_index >= pc.body_count) return;
own = snapshot.data[own_index];
own_shape = body_shapes.data[own_index];
touched = false;
hash_collisions = 0u;
fallback_tests = 0u;
push = ivec3(0);
velocity_change = ivec3(0);
if (takes_part(own, own_shape)) {
own_rotation = rotation_of(own.model);
own_inverse_mass = inverse_mass(own);
if (oversized(own_shape)) {
// Too big for the grid: test every body.
for (uint other = 0u; other < pc.body_count; other++) {
if (takes_part(snapshot.data[other], body_shapes.data[other])) touch_fallback(other);
}
} else {
ivec3 cell = cell_of(own.model[3].xyz);
uint visited[27];
uint visited_count = 0u;
for (int z = -1; z <= 1; z++) {
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
uint hash = hash_cell(cell + ivec3(x, y, z));
// Two neighbour cells can share a hash; visit it once.
bool seen = false;
for (uint index = 0u; index < visited_count; index++) seen = seen || visited[index] == hash;
if (seen) continue;
visited[visited_count++] = hash;
uint count = min(grid_counts.data[hash], CELL_SLOTS);
for (uint entry = 0u; entry < count; entry++) touch_in_cell(grid_slots.data[hash * CELL_SLOTS + entry], cell + ivec3(x, y, z));
}
}
}
for (uint entry = 0u; entry < fallback.data[0]; entry++) touch_fallback(fallback.data[1u + entry]);
}
}
if (hash_collisions > 0u) atomicAdd(event_header.contacts.z, hash_collisions);
if (fallback_tests > 0u) atomicAdd(event_header.contacts.w, fallback_tests);
// Every body is written so the contact flag starts each step fresh; the
// main pass adds collider contacts to it.
precise vec3 position = own.model[3].xyz + vec3(push) * FIXED_STEP;
precise vec3 velocity = own.velocity.xyz + vec3(velocity_change) * FIXED_STEP;
bodies.data[own_index].model[3].xyz = position;
bodies.data[own_index].velocity = vec4(velocity, touched ? 1.0 : 0.0);
}
#endif