Monday, October 6, 2025

The reverse osmosis - when my son gives me clue for different concepts of Computer Graphics - a story of the stuck-ball in a ping-pong game...



The little white circle, known in the digital world as Pingy, was having an existential crisis. He was a ball, and balls were meant to bounce, to fly with crisp, perfect elasticity. Yet, here he was, plastered against the blue paddle named Paddles.

It wasn't a normal contact; it was a cosmic entanglement.

"Let go of me, you big rectangle!" Pingy screamed, vibrating furiously.

Paddles, a Kinematic Body whose every move was dictated by a distant, jittery mouse cursor, didn't mean to hold him. "I'm trying, I swear! I've executed my full impulse to separate us, but the time step is too big!"

Pingy's crisis was the result of a flaw in their digital reality: penetration caused by Discrete Collision Detection (DCD).

The game's clock, ticking at 60 frames per second, was supposed to check Pingy's position at  t1 and then again at t2. But Pingy was fast—too fast. In one time slice, he'd jumped from being outside Paddles to being halfway inside him.

The Pymunk Sequential Impulse Solver, their benevolent god, immediately registered the crime. "Overlap! Apply impulse to separate!"

But before the impulse could fully push Pingy back to the surface, the next time step arrived. Gravity was pulling Pingy down, and Paddles' surface was pushing him up, all while Pingy’s internal velocity was still trying to move him forward.

"I'm stuck in the Zeno's Paradox of physics engines!" Pingy wailed. "I'm forever approaching the exit, but never quite reaching it!"

The paddle surface felt like a sticky, repulsive field. Every millisecond, the solver applied a new impulse, resulting in a rapid, microscopic vibration that translated to an infuriating standstill on the screen.

Suddenly, a change came. A distant developer—their true god—finally adjusted the code, not by implementing the godly Offset Geometric Contact (OGC) model (which was reserved for fancy, billowing capes in ), but by simply making the right choice for the current world:

Python
ball_shape.friction = 0.0

With the removal of that final, tiny opposing force, the Sequential Impulse Solver finally won its tug-of-war. The microscopic forces holding Pingy hostage vanished. The final, forceful impulse took hold.

BWOOONG...

Pingy shot away from Paddles' surface with the perfect, glorious elasticity he was born with. He was flying, bouncing crisply off the walls, a free ball once more.

"I'm un-stuck!" Pingy cheered, realizing the solution to his existential problem wasn't magic, but just a properly applied zero-friction constraint that allowed the Impulse Solver to do its job unimpeded. The world was stable again.

Now some raw technical discussion...

Pymunk, being a wrapper for the Chipmunk engine, relies on an Impulse-Based Solver combined with a simple form of geometric offset to ensure stability.

Pymunk's Technology to Prevent Sticking

The "stuck ball" is a result of Discrete Collision Detection (DCD) failing at high speeds. Pymunk avoids this using a more robust, impulse-based physics loop.

Physics ConceptPymunk Implementation in ScriptHow It Prevents Sticking
1. Sequential Impulse Solverspace.step(1.0 / FPS)This is the core technology. Instead of just pushing objects apart when penetration is detected, the solver iteratively calculates the precise impulse (force X time) needed to satisfy the contact constraints (non-penetration, friction, elasticity) over the duration of the time step. This system is inherently more stable than simple "fix-up" forces.
2. Non-Penetration ConstraintBuilt into the pymunk.Circle and pymunk.Segment shapes.Pymunk treats the shapes as having a defined, non-negotiable surface thickness. When two shapes get too close, the solver activates a distance constraint. This is analogous to a simple, baked-in form of the Offset Geometric Contact (OGC) idea: it ensures the geometric boundaries are never violated.
3. High Elasticityball_shape.elasticity = 1.0In PyMunk, elasticity controls the coefficient of restitution. Setting it to (perfect bounce) ensures that the ball retains all of its kinetic energy after the collision. This prevents the ball from "thudding" and stopping due to energy loss, which often looks like sticking.
4. Zero Frictionball_shape.friction = 0.0Friction is a force that opposes motion between surfaces. Setting it to prevents the ball from "gripping" the paddle surface, which could otherwise slow it down and cause it to appear to halt or slide unnaturally.
5. Kinematic Paddle Controlpaddle_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)By using a Kinematic body and controlling its position directly (paddle_body.position = (clamped_x, PADDLE_Y)), you tell the solver the paddle's movement is guaranteed. This simplifies the physics calculation, allowing the solver to focus purely on resolving the ball's collision against a predictable target, which improves stability.

Why OGC is Not Used (and Why Pymunk is Enough)

While the problem the ball-and-paddle encounters is conceptually the same as what Offset Geometric Contact (OGC) solves, Pymunk/Chipmunk doesn't need to implement the complex OGC model because of three key differences:

  1. Low Dimensionality (2D): Pymunk is a 2D engine. OGC is primarily designed for complex 3D simulations of co-dimensional objects (thin cloth, shells, etc.) where penetration is far more difficult to detect and resolve.

  2. Simple Geometry: Your simulation uses simple geometries (circles and line segments). Pymunk's solver is highly optimized for these shapes.

  3. Efficiency over Guarantee: OGC provides a mathematical guarantee of zero penetration, which is extremely expensive. Pymunk prioritizes real-time efficiency and uses its impulse solver to achieve a highly reliable near-zero penetration state, which is perfectly sufficient for games.

In short, Pymunk uses a combination of an Impulse-Based Solver and Constraint Satisfaction to ensure the ball never violates the surface of the paddle, making the expensive, complex methods like OGC unnecessary for a simple, stable game.

The Source Code...

import pymunk
import pymunk.pygame_util
import pygame
# 1. Import the DrawOptions class for modern Pymunk drawing
from pymunk.pygame_util import DrawOptions

pygame.init()
screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Pymunk Ping Pong (No Sticking)")

# --- Physics Space Setup ---
space = pymunk.Space()
# For classic ping pong, set gravity to (0, 0)
space.gravity = (0, 0)

# --- Ball: Dynamic Circle ---
mass = 1
radius = 10
moment = pymunk.moment_for_circle(mass, 0, radius)
ball_body = pymunk.Body(mass, moment)
ball_body.position = (screen_width // 2, screen_height // 2)

# Give the ball an initial velocity to start the game
ball_body.velocity = (300, 200)

ball_shape = pymunk.Circle(ball_body, radius)
ball_shape.elasticity = 1.0 # Perfect bounce for ping pong
ball_shape.friction = 0.0 # No friction to maintain speed
ball_shape.color = (255, 255, 255, 255) # White ball
space.add(ball_body, ball_shape)

# --- Paddle (Player): Kinematic Body ---
# Use a Kinematic body so we can move it manually without physics forces
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_Y = screen_height - 50

paddle_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
paddle_body.position = (screen_width // 2, PADDLE_Y)

# Segment shape for the paddle surface
paddle_shape = pymunk.Segment(
paddle_body,
(-PADDLE_WIDTH // 2, 0), # Start relative to body center
(PADDLE_WIDTH // 2, 0), # End relative to body center
PADDLE_HEIGHT // 2 # Thickness
)
paddle_shape.elasticity = 1.0
paddle_shape.friction = 0.0
paddle_shape.color = (0, 100, 255, 255) # Blue paddle
space.add(paddle_body, paddle_shape)

# --- Walls/Boundaries (Static Segments) ---
# Create four static segments to form the screen boundaries
boundary_lines = [
pymunk.Segment(space.static_body, (0, 0), (screen_width, 0), 1), # Top
pymunk.Segment(space.static_body, (0, screen_height), (screen_width, screen_height), 1), # Bottom
pymunk.Segment(space.static_body, (0, 0), (0, screen_height), 1), # Left
pymunk.Segment(space.static_body, (screen_width, 0), (screen_width, screen_height), 1) # Right
]
for line in boundary_lines:
line.elasticity = 1.0
line.friction = 0.0
line.color = (100, 100, 100, 255)
space.add(line)

# --- Game Loop ---
running = True
clock = pygame.time.Clock()
FPS = 60
# 2. Initialize DrawOptions, passing the screen surface
options = DrawOptions(screen)

while running:
# --- Input Handling ---
mouse_x, _ = pygame.mouse.get_pos()

for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Move paddle to follow the mouse's X position
target_x = mouse_x

# Clamp paddle position within screen boundaries
min_x = PADDLE_WIDTH // 2
max_x = screen_width - PADDLE_WIDTH // 2
clamped_x = max(min_x, min(target_x, max_x))

# Move the kinematic body
paddle_body.position = (clamped_x, PADDLE_Y)

# --- Physics Step (Solver) ---
# The time step duration is passed as the argument
space.step(1.0 / FPS)

# --- Drawing ---
screen.fill((10, 20, 30)) # Dark blue background

# 3. Call debug_draw() on the space, passing the options
space.debug_draw(options)

pygame.display.flip()
clock.tick(FPS)

pygame.quit()