Ice & Pebble Physics Reference

How stones move, curl, slow down, and how the 2D pebble evolves across a game.

120 Hz fixed step 2D sheet Pebble wear Sweep film Deterministic inputs

This page documents assets/config.toml options and the physics performed each fixed step.

What happens each physics step

  1. Film decay: the sweeping “water film” on the sheet decays exponentially toward 0 with time constant film_decay_s.
  2. Per-stone integration: for each moving stone:
    1. Sample the local surface at the stone to get multipliers for friction, curl and angular damping, plus a tiny “cross‑slope” lateral bias.
    2. Apply forces:
      • Drag (linear + quadratic): opposes motion, magnitudes lin_drag · |v| and quad_drag · |v|².
      • Base (Coulomb‑like) friction: mu_base · g opposing motion.
      • Curl force: perpendicular to motion, magnitude curl_coeff · effective_spin · |v| · curl_speed_factor(|v|).
      • Cross‑slope bias: a small extra push to the left of travel proportional to local slope and speed.

      curl_speed_factor(|v|) = 1 / (1 + curl_speed_suppression · |v|); when suppression is zero the factor stays at 1.

    3. Integrate with explicit Euler; snap to full stop if a drag reversal would flip velocity this frame.
    4. Angular damping: ω *= clamp(1 − ang_drag · dt, 0, 1); advance heading by ω · dt.
    5. Bounds: mark out of play if the stone crosses back lines or boards.
    6. Sleep: if speed stays below 0.2 m/s for 0.25 s, snap v=0, ω=0.
  3. Wear: shave pebble under the running band by a small amount proportional to distance traveled this step.
  4. Sweeping: when active, ahead of the stone: slightly flatten the pebble and charge the temporary film (which then decays).
  5. Collisions: resolve elastic disc–disc contacts (unit restitution on the normal; no angular impulse).

Effective spin mapping (handles very low/high rotation gracefully)

The curl strength uses a smoothed “effective spin” (in the direction of ω):

reference = curl_spin_reference
min_value = curl_spin_min
max_value = curl_spin_max

gain      = ((reference - min_value) * reference) / (max_value - reference)
blend     = gain / (|ω| + gain)              // 0..1
eff_mag   = min_value + (max_value - min_value) * blend
effective_spin       = eff_mag * sign(ω)
variation_multiplier = eff_mag / reference    // scales lateral-bias response

Low |ω| → effective_spin ≈ max; high |ω| → ≈ min. If |ω| is essentially zero, curl is 0 for that instant but the bias scaling uses max/reference.

Config reference (assets/config.toml)

[match]

KeyWhat it does
ends_to_playNumber of regulation ends before extras are considered.
stones_per_teamStones each team may throw per end.
hammerTeam with the hammer to start ("Red" or "Yellow").
ice_presetSelects one of the base ice blocks below (ice_fast, ice_average, ice_slow).

[ice_*] — Base ice (global)

These set the global “feel” of the sheet. Local variability from pebble multiplies these.

KeyDescription
mu_baseSpeed‑independent drag term (Coulomb‑like). Part of the base opposing force.
curl_coeffScale for the perpendicular curl force.
lin_dragSpeed‑proportional drag.
quad_dragSpeed‑squared drag for higher velocities.
ang_dragAngular damping per second.
curl_speed_suppressionk in 1 / (1 + k · |v|); higher values reduce curl at higher speeds.
curl_spin_referenceSpin (rad/s) at which curl matches your baseline.
curl_spin_minMinimum effective curl at very high |ω|.
curl_spin_maxMaximum effective curl near zero |ω|.

[pebble] — 2D sheet, wear & sweeping

Grid & seeding

cell_m
Grid spacing (m) for the pebble field.
init_mean_um
Target average pebble height (μm) when seeding.
init_std_um
Standard deviation of initial noise (μm).
noise_octaves
Layers of low‑frequency fractal noise for smooth variation.
height_clip_um
Clamp for deviations ± from the mean (μm).
seed
RNG seed for deterministic pebble generation.
randomize_seed_each_game
When true, pick a new seed every game.
persist_across_ends
Keep worn pebble between ends (otherwise re‑seed).

Coupling local height → physics

Let h be the local height average over the running band, and dh = (h − mean)/clip clamped to a sane range. The surface model returns per‑point multipliers:

KeyEffect
mu_alphaScales mu_base by ~(1 + mu_alpha · dh).
lin_alphaScales lin_drag by ~(1 + lin_alpha · dh).
quad_alphaScales quad_drag by ~(1 + quad_alpha · dh).
curl_alphaScales curl_coeff by ~(1 + curl_alpha · dh).
ang_damp_alphaScales ang_drag by ~(1 + ang_damp_alpha · dh).

Exact clamping/limits are handled in code to avoid non‑physical values.

Cross‑slope drift (micro lateral bias)

We compute a normalized cross‑slope from the local gradient (projected onto the left‑normal of travel). This produces a tiny lateral acceleration per unit speed:

KeyMeaning
side_grad_gammaGain from normalized cross‑slope to lateral bias.
side_grad_spin_refSpin (rad/s) where drift is halved.
side_grad_spin_powerHow quickly drift falls with spin.
side_grad_min_factorFloor for drift even at heavy rotation.

Wear (traffic)

Each step, we shave pebble under the running band:

travel = |v| · dt
wear   = wear_um_per_pass · travel · (1 + 0.1 · |ω|)

radius = running_band_radius · wear_radius_scale
apply  wear uniformly inside that disk at the current stone position
KeyMeaning
wear_um_per_passMicrons flattened per metre traveled under the band.
wear_radius_scaleExpand/shrink the wear footprint vs the band radius.

Sweeping (ahead of the stone)

When sweeping is active, we act at a point ahead of the stone:

center = pos + normalize(v) · lookahead_m
radius = sweep_radius_m

// Permanent effect:
flatten += sweep_flatten_um_per_s · dt

// Temporary effect:
film := min(1, film + Δ)                      // charged while sweeping
film → decays exponentially with time constant film_decay_s
// The surface model uses film (0..1) to reduce friction by film_friction_scale
KeyMeaning
lookahead_mDistance in front of the stone where sweeping acts.
sweep_radius_mRadius of the sweep zone.
sweep_flatten_um_per_sMicrons per second of permanent flattening while sweeping.
film_friction_scaleHow strongly film reduces friction (0 = none, 1 = full).
film_decay_sExponential decay constant for film once sweeping stops.

Forces & integration (formulas)

// Inputs at sample point:
v         = current linear velocity (2D)
speed     = |v|
dir       = normalize(v)
left      = (-dir.y, dir.x)     // left-normal
local     = surface.sample(pos, v, ω, heading)

// Local multipliers supplied by the surface model:
μ_scale, lin_scale, quad_scale, curl_scale, ang_scale, bias_per_speed = local

// Drag (already multiplied by mass in code):
F_lin   = lin_drag  · speed          · m · lin_scale
F_quad  = quad_drag · speed²         · m · quad_scale
F_base  = mu_base   · g              · m · μ_scale
F_drag  = -(F_lin + F_quad + F_base) · dir

// Curl (mass-independent term in code; acceleration comes after /m):
spin    = effective_spin(ω)          // see mapping above
curl_speed_factor = 1 / (1 + curl_speed_suppression · speed)
F_curl  = (curl_coeff · curl_scale · spin · speed · curl_speed_factor) · left

// Cross-slope micro-bias:
F_bias  = (bias_per_speed · speed · m · variation_multiplier(spin)) · left

// Integrate:
a       = (F_drag + F_curl + F_bias) / m
v'      = v + a · dt
pos'    = pos + v · dt
ω'      = ω * clamp(1 - ang_drag · ang_scale · dt, 0, 1)

curl_speed_factor suppresses curl at higher speeds; when curl_speed_suppression = 0 the factor stays at 1.

If v' · v ≤ 0 (drag reversal), we snap the stone to rest to avoid jitter. Sleep triggers if speed stays below threshold for the configured window.

Practical tuning tips

  • Overall pace: start with mu_base, lin_drag, quad_drag. Get hog‑to‑hog times right first.
  • Curl shape: use curl_coeff for magnitude; then refine the feel with curl_spin_* to shape late vs early finish.
  • Consistency vs traffic: increase wear_um_per_pass for more visible tracks; slightly reduce curl_alpha if worn paths feel too “grabby”.
  • Sweeping feel: film_friction_scale gives the immediate glide benefit; sweep_flatten_um_per_s makes paths play straighter next stone.
  • Micro‑bias realism: small side_grad_gamma (≈ 0.02–0.10) with sensible spin falloff produces believable, non‑twitchy drift.

Geometry & constants

  • Sheet: 45.720 m × 4.750 m; near hog at x=0; far hog at x=21.945.
  • Running‑band diameter constant: 0.127 m (used for wear/sampling radius).
  • Stones default to 19 kg, radius 0.1455 m.