-
Notifications
You must be signed in to change notification settings - Fork 191
Description
I hope you are doing well.
I’m encountering an unexpected instability in my Pymunk simulation and would appreciate your insight. In my setup, I have three shapes aligned in a row: two dynamic bodies with 0pi and 1pi moving toward a static body with 0.5pi placed between them. Gravity is set to zero, and each body has mass = 1, so there is no built‑in friction. To emulate friction during collisions, I apply a compensating impulse inside the post_solve callback. In most configurations, this works perfectly; however, whenever the three collision contact points become colinear, the simulation exhibits oscillatory, unstable behavior.
Below is a minimal example of the relevant code:
import numpy as np
import pymunk
# Build 14 discrete direction angles (in degrees)
COLLISION_IMPACT_FORCE = 1.42 # Threshold impact force above which special logic applies
resistance = [
1.22, 1.09, 1.03, 1.45, 1.43, 1.19, 1.06,
1.11, 1.06, 1.19, 1.43, 1.45, 1.03, 1.09
]
friction = 1.45
# Define base directions for half the angles (8 cardinal and intercardinal directions)
base_dirs = np.array([
90.0, # Upwards (positive Y axis)
67.6, # Northeast offset by 22.4 degrees from vertical
42.1, # Northeast offset by 47.9 degrees from vertical
0.0, # Rightwards (positive X axis)
-19.5, # Southeast offset by 19.5 degrees below horizontal
-42.1, # Southeast offset by 47.9 degrees below horizontal
-67.6, # Southeast offset by 22.4 degrees from vertical downward
-90.0 # Downwards (negative Y axis)
])
# Reflect base directions about the horizontal axis to get the remaining 6 symmetric angles
reflected = 180.0 - base_dirs
# Exclude duplicate vertical directions at 90° and -90° (270°)
mask = (np.abs(reflected - 90.0) > 1e-6) & (np.abs((reflected % 360) - 270.0) > 1e-6)
reflected = reflected[mask]
# Combine base and reflected directions, wrap to [0, 360)
dirs14 = np.mod(np.concatenate([base_dirs, reflected]), 360.0)
# Construct a lookup table with linear interpolation for resistance at each integer degree from 0° to 359°
angles = np.concatenate([dirs14, [dirs14[0] + 360.0]]) # Close the loop at 360°
vals = np.concatenate([resistance, [resistance[0]]])
degs = np.arange(360)
resistance360 = np.interp(degs, angles, vals) # Array of length 360, resistance for each degree
def get_resistance(local_ang: float) -> float:
"""
Map any angle in [0, 360) to the nearest integer degree
and return the corresponding resistance value from the lookup table.
"""
deg = int(round(local_ang)) % 360
return resistance360[deg]
def agent_post_solve(arbiter: pymunk.Arbiter, space, data):
"""
Callback executed after collision resolution.
Applies compensating impulses or forces to simulate friction
based on the direction and magnitude of the collision impulse.
"""
s1, s2 = arbiter.shapes
impulse = arbiter.total_impulse
force = impulse * fps # Convert impulse to force via frame rate
# Retrieve the first contact point in world coordinates
cps = arbiter.contact_point_set # Get ContactPointSet object
pts = cps.points # Tuple of ContactPoint objects
if pts:
cp = pts[0] # Use the first contact point
# Choose either cp.point_a, cp.point_b, or their midpoint for world coordinates
point = (cp.point_a + cp.point_b) * 0.5
else:
# Fallback: apply at the shape's center of mass if no contact points are available
point = s1.body.position
# Determine which shape is being hit on its side by converting the contact point to local coordinates
local_point = s1.body.world_to_local(point)
if -8 < local_point.x < 8:
# Contact is near the center: treat as a head-on collision
act_impulse = impulse
act_force = force
s1, s2 = arbiter.shapes
else:
# Contact is on the side: reverse impulse direction for compensation
act_impulse = -impulse
act_force = -force
s2, s1 = arbiter.shapes
id1, id2 = s1.collision_type, s2.collision_type
# Calculate local impulse angle to determine directional resistance
local_imp = impulse.rotated(-s1.body.angle)
local_ang = local_imp.angle_degrees % 360.0
r = get_resistance(local_ang)
# Precompute body centers for direct impulse application
point1 = s1.body.position
point2 = s2.body.position
# If resistance exceeds the collision impact threshold, apply full stop logic
if r > COLLISION_IMPACT_FORCE:
if agents_state[id1] == 0 or agents_state[id2] == 0:
# One of the agents is stationary; apply a full compensating impulse at the centers to ensure immediate stop.
print('Act impulse', impulse)
s1.body.apply_impulse_at_world_point(-act_impulse, point1)
s2.body.apply_impulse_at_world_point(-act_impulse, point2)
else:
# Both agents are moving; apply opposing impulses at the contact point
s1.body.apply_impulse_at_world_point(-act_impulse, point)
s2.body.apply_impulse_at_world_point(act_impulse, point)
else:
# Resistance is below threshold; apply partial friction compensation
friction_impulse = act_impulse * (r / COLLISION_IMPACT_FORCE)
if agents_state[id1] == 0 or agents_state[id2] == 0:
# One agent is stationary; apply friction impulse at centers
s1.body.apply_impulse_at_world_point(-friction_impulse, point1)
s2.body.apply_impulse_at_world_point(-friction_impulse, point2)
else:
# Both agents moving; apply friction impulse at contact point
s1.body.apply_impulse_at_world_point(-friction_impulse, point)
s2.body.apply_impulse_at_world_point(friction_impulse, point)
return
# Simulation parameters
num_agents = 3
num_init_fixed_light_source = 5
center_motion = -1
# Initial positions of agents (x, y)
agents_positions = np.asarray([[100, 200], [300, 200], [200, 200]])
# Initial angles of agents in radians
agents_angle = np.asarray([0* np.pi, 1 * np.pi, 0.5 * np.pi])
# Agent states: 1 = forward motion, 0 = static
agents_state = np.asarray([1, 1, 0])
Screen.Recording.2025-07-16.at.09.43.04.mov
Questions:
Why does aligning the three contact points in a straight line lead to this unstable, oscillatory response?
Would you recommend a different approach to avoid this instability?
Thank you very much for your time and help. Any guidance or references you can share would be extremely valuable.
Best regards,
Carl