#version 450
layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in;
#include <physics_abi.glsl>
#include <physics_shapes.glsl>
struct ConditionInstruction {
uvec4 words;
vec4 values;
};
struct RuleState {
uvec4 config;
vec4 timing;
uvec4 state;
};
layout(set = 0, binding = 1) readonly buffer Conditions { ConditionInstruction data[]; } conditions;
layout(set = 0, binding = 2) buffer Rules { RuleState data[]; } rules;
layout(set = 0, binding = 5) readonly buffer Commands { BodyCommand data[]; } commands;
struct Collider {
mat4 model;
vec4 shape;
vec4 velocity;
vec4 material;
uvec4 layers;
};
layout(set = 0, binding = 7) readonly buffer Colliders { Collider data[]; } colliders;
struct ColliderNode {
vec4 lower;
vec4 upper;
// x = first node after this subtree; y = collider index for a leaf,
// or UINT_MAX for an internal node; z = first collider index after it.
uvec4 links;
};
layout(set = 0, binding = 12) readonly buffer ColliderTree { ColliderNode data[]; } collider_tree;
void collider_distance(Collider collider, vec3 point, out vec3 normal, out float distance) {
shape_distance(collider.shape, mat3(collider.model), collider.model[3].xyz, point, normal, distance);
}
// Visits nearby CPU/static colliders in their stable upload order. A node's
// sphere test is conservative for every shape it contains. The body's
// rotation only shapes its reach along each contact normal; contacts apply
// no torque.
void collide_with_colliders(inout PhysicsState body, BodyShape own) {
if (pc.collider_count == 0u) return;
mat3 rotation = rotation_of(body.model);
float radius = bounding_radius(own.shape);
uint node_index = 0u;
uint next_collider = 0u;
uint node_count = pc.collider_count * 2u - 1u;
while (node_index < node_count) {
ColliderNode node = collider_tree.data[node_index];
if (node.links.z <= next_collider) {
node_index = node.links.x;
continue;
}
vec3 center = body.model[3].xyz;
vec3 nearest = clamp(center, node.lower.xyz, node.upper.xyz);
precise vec3 delta = center - nearest;
precise float radius_squared = radius * radius;
if (sim_dot(delta, delta) > radius_squared) {
next_collider = node.links.z;
node_index = node.links.x;
continue;
}
if (node.links.y == 0xffffffffu) {
node_index++;
continue;
}
uint index = node.links.y;
next_collider = index + 1u;
node_index = node.links.x;
Collider collider = colliders.data[index];
if ((own.layers.x & collider.layers.y) == 0u || (collider.layers.x & own.layers.y) == 0u) continue;
vec3 normal;
float distance;
collider_distance(collider, body.model[3].xyz, normal, distance);
precise float depth = support(own.shape, rotation, normal) - distance;
if (depth <= 0.0) continue;
precise vec3 position = body.model[3].xyz + normal * depth;
body.model[3].xyz = position;
// The correction can move the body into a later collider whose tree
// branch was skipped. Re-query from the root, but do not revisit any
// collider the original list-order loop already passed.
node_index = 0u;
body.velocity.w = 1.0;
precise vec3 relative = body.velocity.xyz - collider.velocity.xyz;
float closing = sim_dot(relative, normal);
if (closing >= 0.0) continue;
// Bounce only above 1 m/s so resting contacts settle.
float restitution = closing < -1.0 ? max(own.material.y, collider.material.y) : 0.0;
precise vec3 tangent = relative - normal * closing;
float tangent_speed = sim_length(tangent);
if (tangent_speed > 0.000001) {
precise float friction_squared = own.material.x * collider.material.x;
float friction = sim_sqrt(friction_squared);
relative -= sim_div(tangent, tangent_speed) * min(tangent_speed, -friction * closing);
}
relative -= normal * closing * (1.0 + restitution);
precise vec3 velocity = relative + collider.velocity.xyz;
body.velocity.xyz = velocity;
}
}
bool is_dynamic(PhysicsState body) {
return body.properties.z > 0.5 && body.properties.z < 1.5 && body.properties.x > 0.0;
}
void apply_command(inout PhysicsState body, BodyCommand command) {
// The CPU checked the generation too; this guards against a table
// rebuilt between the check and the dispatch.
if (command.header.z != body.metadata.y) return;
uint kind = command.header.y;
if (kind == COMMAND_TELEPORT) {
body.model = mat4(command.values[0], command.values[1], command.values[2], command.values[3]);
} else if (kind == COMMAND_SET_VELOCITY) {
body.velocity.xyz = command.values[0].xyz;
body.angular_velocity.xyz = command.values[1].xyz;
} else if (kind == COMMAND_IMPULSE && is_dynamic(body)) {
precise vec3 velocity = body.velocity.xyz + sim_div(command.values[0].xyz, body.properties.x);
body.velocity.xyz = velocity;
} else if (kind == COMMAND_FORCE && is_dynamic(body)) {
precise vec3 velocity = body.velocity.xyz + sim_div(command.values[0].xyz, body.properties.x) * pc.dt;
body.velocity.xyz = velocity;
} else if (kind == COMMAND_SET_CUSTOM_VALUES) {
body.custom_values = command.values[0];
}
}
void apply_commands(inout PhysicsState body, uint body_index) {
// This step's commands are sorted by body index: find this body's first.
uint end = pc.command_first + pc.command_count;
uint low = pc.command_first;
uint high = end;
while (low < high) {
uint middle = (low + high) / 2u;
if (commands.data[middle].header.x < body_index) low = middle + 1u;
else high = middle;
}
for (uint index = low; index < end && commands.data[index].header.x == body_index; index++) {
apply_command(body, commands.data[index]);
}
}
float read_field(PhysicsState body, uint field) {
vec3 position = body.model[3].xyz;
vec3 scale = vec3(sim_length(body.model[0].xyz), sim_length(body.model[1].xyz), sim_length(body.model[2].xyz));
if (field == 0u) return position.x;
if (field == 1u) return position.y;
if (field == 2u) return position.z;
if (field == 3u) return body.velocity.x;
if (field == 4u) return body.velocity.y;
if (field == 5u) return body.velocity.z;
if (field == 6u) return body.angular_velocity.x;
if (field == 7u) return body.angular_velocity.y;
if (field == 8u) return body.angular_velocity.z;
if (field == 9u) return scale.x;
if (field == 10u) return scale.y;
if (field == 11u) return scale.z;
if (field == 12u) return body.properties.x;
if (field == 13u) return body.properties.y;
if (field == 14u) return sim_length(body.velocity.xyz);
if (field >= 0x100u && field < 0x104u) return body.custom_values[field - 0x100u];
return 0.0;
}
bool compare_value(float left, float right, uint comparison) {
if (comparison == 0u) return left < right;
if (comparison == 1u) return left <= right;
if (comparison == 2u) return left > right;
if (comparison == 3u) return left >= right;
precise float difference = abs(left - right);
if (comparison == 4u) return difference <= 0.00001;
return difference > 0.00001;
}
bool evaluate_condition(PhysicsState body, uint offset, uint count) {
bool stack[64];
uint stack_size = 0u;
for (uint index = 0u; index < count && index < 64u; index++) {
ConditionInstruction instruction = conditions.data[offset + index];
uint operation = instruction.words.x;
if (operation == 1u) {
stack[stack_size++] = compare_value(
read_field(body, instruction.words.y),
instruction.values.x,
instruction.words.z
);
} else if (operation == 2u) {
float value = read_field(body, instruction.words.y);
stack[stack_size++] = value >= instruction.values.x && value <= instruction.values.y;
} else if (operation == 3u) {
stack[stack_size++] = body.velocity.w > 0.5;
} else if (operation == 4u) {
stack[stack_size++] = sim_length(body.velocity.xyz) < 0.02 && sim_length(body.angular_velocity.xyz) < 0.02;
} else if (operation == 5u) {
stack[stack_size++] = pc.elapsed >= instruction.values.x;
} else if (operation == 16u && stack_size >= 2u) {
bool right = stack[--stack_size];
stack[stack_size - 1u] = stack[stack_size - 1u] && right;
} else if (operation == 17u && stack_size >= 2u) {
bool right = stack[--stack_size];
stack[stack_size - 1u] = stack[stack_size - 1u] || right;
} else if (operation == 18u && stack_size >= 1u) {
stack[stack_size - 1u] = !stack[stack_size - 1u];
}
}
return stack_size == 1u && stack[0];
}
vec4 event_payload(PhysicsState body, uint payload_kind) {
if (payload_kind == 1u) return vec4(body.model[3].xyz, 1.0);
if (payload_kind == 2u) return body.velocity;
if (payload_kind == 3u) return body.angular_velocity;
if (payload_kind == 5u) return body.custom_values;
return vec4(0.0);
}
void main() {
uint body_index = gl_GlobalInvocationID.x;
if (body_index >= pc.body_count) return;
PhysicsState body = bodies.data[body_index];
if (pc.command_count > 0u) apply_commands(body, body_index);
// The contact pass before this one already reset velocity.w.
// properties.z: 0 = fixed, 1 = dynamic, 2 = kinematic.
// Solver 3 is Custom: its own shader moves the body after this step.
precise vec3 from_solver = abs(body.custom_values.xxx - vec3(2.0, 3.0, 4.0));
if (is_dynamic(body) && from_solver.y > 0.5) {
precise vec3 velocity = body.velocity.xyz;
if (from_solver.z < 0.5) {
// Space mode attracts bodies toward the origin. The force is
// softened near the target so bodies do not explode numerically.
vec3 to_target = -body.model[3].xyz;
float distance_squared = sim_dot(to_target, to_target);
if (distance_squared > 0.000001) {
vec3 direction = sim_normalize(to_target);
float safe_distance_squared = max(distance_squared, 4.0);
velocity += direction
* sim_div(500.0, safe_distance_squared)
* body.properties.y * pc.dt;
}
} else {
velocity += vec3(pc.gravity_x, pc.gravity_y, pc.gravity_z)
* body.properties.y * pc.dt;
}
precise vec3 position = body.model[3].xyz + velocity * pc.dt;
body.velocity.xyz = velocity;
body.model[3].xyz = position;
BodyShape own = body_shapes.data[body_index];
// Solver 2 is NoCollision.
if (own.shape.x < 2.5 && from_solver.x > 0.5) {
collide_with_colliders(body, own);
}
}
bodies.data[body_index] = body;
uint rule_offset = body.metadata.z;
uint rule_count = body.metadata.w;
for (uint local_rule = 0u; local_rule < rule_count; local_rule++) {
uint rule_index = rule_offset + local_rule;
RuleState rule = rules.data[rule_index];
bool current = evaluate_condition(body, rule.config.x, rule.config.y);
bool previous = rule.state.z != 0u;
bool already_emitted = rule.state.y != 0u;
bool should_emit = false;
if (rule.config.w == 0u) should_emit = current && !previous;
else if (rule.config.w == 1u) should_emit = !current && previous;
else if (rule.config.w == 2u) should_emit = current;
else if (rule.config.w == 3u) should_emit = current && !already_emitted;
precise float since_emitted = pc.elapsed - rule.timing.y;
bool cooldown_ready = since_emitted >= rule.timing.x;
if (should_emit && cooldown_ready) {
emit_event(body, rule.config.z, rule.state.x, event_payload(body, rule.state.x));
rule.timing.y = pc.elapsed;
rule.state.y = 1u;
}
rule.state.z = current ? 1u : 0u;
rules.data[rule_index] = rule;
}
}