The published work on cue-sports physics, and how this engine implements it.
This page has two halves and a join. Part I is a review of the literature — written to teach the physics rather than to name papers, so the slip decay, the 5/7 and 2/7 factors, the 90-degree rule and the throw cap are derived here rather than asserted. Part II describes what this engine actually does, with the model, the equations and the constants, and cites the source file and line for every non-trivial claim. Between them is a list of everything in the review the engine has not taken up.
Where a source could not be verified, the review says so rather than quietly citing it. Three attributions in common circulation did not survive checking and are corrected in place.
What is known about how billiard balls behave, and how simulators have modelled it.
A literature review for the background and related-work sections of a paper on a VR cue-sports engine
Compiled September 2026.
This review covers the physics of billiards, pool and snooker and the numerical methods used to simulate it, with a short section on the VR-specific problems of tracking a hand-held implement. It is written to be read and learned from: the important relations are derived rather than merely named, so that a reader coming from graphics or HCI rather than from contact mechanics can follow the argument.
Cue sports occupy an unusual position in the sports-physics literature. The core problem — rigid spheres of equal mass, rolling and sliding on a nearly flat, nearly uniform surface, colliding nearly elastically — is clean enough that closed-form answers exist for most of it, which is why it has been a physics-teaching staple since 1835. But the field is small, and a great deal of the usable knowledge lives outside the peer-reviewed literature, in books by enthusiast-engineers, in a long-running instructional magazine column, and in the documentation of open-source simulators. Any honest review must say which is which, so the source type is stated for every reference:
Where a numerical value is quoted, its source and the spread between sources are given. Where the literature is genuinely thin, this is stated rather than papered over. Sections 3 (cushions), 4 (pockets) and 6 (VR implements) contain the largest gaps, and those gaps are flagged explicitly in §8.
Gaspard-Gustave de Coriolis published Théorie mathématique des effets du jeu de billard in Paris in 1835 [1]. It is a 208-page monograph, not a paper, and its structure is worth setting out because the standard secondary description of it ("the first mathematical treatment of billiards") badly undersells its scope. The chapter list, taken from the scanned first edition held by the Bibliothèque nationale de France [1], is:
| Chapter | p. | |
|---|---|---|
| — | Exposé des principales conséquences de la théorie... (a non-mathematical summary of results and the geometric constructions that give the ball motions) | 1 |
| I | Du mouvement d'une bille sur un plan horizontal en ayant égard aux frottements — motion of a ball on a horizontal plane, accounting for friction | 51 |
| II | De l'effet du coup de queue horizontal — the effect of a level cue stroke | 78 |
| III | Du choc de deux billes et du carambolage, en négligeant le frottement très-petit qui a lieu entre les billes pendant le choc — two-ball collision and the carom, neglecting the small inter-ball friction | 101 |
| IV | Des effets d'un deuxième choc entre deux billes, après un premier choc — the effects of a second ball-ball collision | 112 |
| V | Du choc de deux billes, en ayant égard au frottement entre les billes pendant le choc, au défaut d'élasticité, et à l'inégalité des masses — two-ball collision including inter-ball friction, imperfect elasticity, and unequal masses | 119 |
| VI | Du choc contre la bande, soit directement, soit après un autre choc — impact against the cushion, direct or following another impact | 135 |
| VII | Cas particulier où il faut modifier les formules... relatives à l'effet du frottement pendant le choc — the special case requiring modified friction formulae | 143 |
| VIII | De l'effet du coup de queue incliné — the effect of an elevated (inclined) cue stroke | 146 |
So Coriolis did not merely write down the frictionless two-ball collision. He treated ball–cloth friction, the tip impulse, the frictional and inelastic ball–ball collision with unequal masses, the cushion impact, and the massé shot — that is, the whole problem, in an order a modern simulator author would recognise. Chapters III and V are structured exactly as a modern paper would be: solve the idealised case first, then add friction and restitution as corrections.
Four results from the book are still routinely quoted, and the clearest modern restatement of them is Alciatore's survey article [2] (a practitioner-authored technical article, self-published on the author's site after an AJP submission):
Coriolis' book was not widely read in the Anglophone world because it was never translated until David Nadler produced an English edition in 2005 [3] [B]. That translation is the reason the 1835 results re-entered the modern literature; both Mathavan et al. [4,5] and Alciatore [2] cite Nadler rather than the French original.
Why it is still cited. Three reasons. First, priority: almost every relation in §§1–2 below appears in the 1835 book. Second, completeness: Coriolis treated the frictional collision and the elevated cue, problems that most twentieth-century papers dropped in favour of the frictionless case. Third, and most practically, his geometric constructions are still the fastest route to a qualitative answer, and the modern papers explicitly benchmark against his conclusions (Mathavan et al. [4] compare their measured cushion restitution to Coriolis' value; Alciatore [2] provides a "modern proof for the numbers in Coriolis' 4th conclusion").
A ball on the table has thirteen numbers of interest but only six degrees of freedom: centre-of-mass position and velocity , and orientation and angular velocity . Because the ball is a homogeneous sphere its inertia tensor is isotropic,
which is derived in full in Shepard [6] (§1, Problem 1.3) [C]. Isotropy is the single most important structural fact in billiard physics: is always parallel to , so angular momentum and angular velocity never separate, there is no precession, and every rotational equation reduces to a scalar multiple. A simulator can carry as a plain 3-vector and never needs an inertia tensor rotation.
The quantity that governs everything on the cloth is the contact-point slip velocity. With up and the contact point at relative to the centre, the velocity of the material point of the ball momentarily at the contact is
If the ball rolls; otherwise it slides. The rolling condition written out for straight-line motion is the familiar — a ball moving at whose surface is turning at lays its surface down on the cloth without scuffing. Note that is unaffected by the component of along : sidespin about the vertical axis does not appear in the slip velocity at all, because the contact is (idealised as) a point on the spin axis. This is why vertical-axis spin has to be treated as a separate third regime (§1.5), and why the point-contact idealisation eventually has to be broken.
Take a sliding ball. Coulomb friction acts at the contact, opposing the slip:
Linear and angular accelerations follow:
Differentiate the slip:
Two consequences, and they are the reason billiards is analytically tractable at all:
(a) The slip direction is constant during a sliding phase. is antiparallel to , so never rotates; only shrinks, linearly, at rate . The friction force is therefore constant in both magnitude and direction for the whole slide. Constant acceleration plus an initial velocity gives a parabola — which is Coriolis' first conclusion, and also the reason that the swerve of a massé shot is parabolic. Alciatore [2] notes that infrared high-speed video of the frictional heat trace on the cloth shows the parabola directly.
(b) The slide has a closed-form duration and length. Sliding ends at
For collinear motion (topspin/backspin only, no sidespin) the same result can be reached by eliminating the impulse between the linear and angular equations, as Shepard does [6, §2]:
a relation independent of and of . Imposing the rolling condition gives the natural-roll speed
This is where the 5/7 and 2/7 come from. Special cases worth committing to memory:
The 7R/5 height. A horizontal cue impulse delivered at height above the cloth produces and, by moment about the centre, . Demanding immediate natural roll, , gives
This is the ball's centre of percussion for a horizontal impulse. It appears again, and importantly, in the cushion discussion (§3.2). Salazar and Sánchez-Lavega [7] [P] give the general treatment of a ball struck by a tapering rod, and this is the result Mathavan et al. cite for it [4].
A rolling ball still decelerates, because the cloth is deformable. The ball sinks into the nap; the contact patch is not a point but a small region, and the pressure distribution over it is fore-aft asymmetric because the cloth fibres relax more slowly than they compress. The resultant normal reaction therefore acts ahead of the contact centre and tilted from the vertical by a small angle , giving a retarding horizontal component and a torque opposing . Mathavan et al. [4] give this picture explicitly, citing the rolling-friction treatments of Hierrezuelo and Carnero [8] [P] and Doménech, Doménech and Cebrián [9] [P].
The engineering consequence is that rolling resistance is well modelled as a constant deceleration that does not depend on speed [4, citing 8]. Shepard [6] parameterises the same thing as table speed, defined as , measured by rolling a ball distance in time to a stop, . He gives: very slow table 50–70, normal 80–100, fast pool table above 120, fast carom table above 150 — i.e. from about down to below .
Because vertical-axis spin does not enter the slip velocity of a point contact, a point-contact model predicts that sidespin never decays — which is plainly false. The resolution is that the contact is a patch of radius , and the tangential tractions over the patch produce a drilling (spinning) friction torque. For a circular patch of radius under uniform pressure,
a constant angular deceleration. Simulators generally absorb into a phenomenological dimensionless "spinning friction" coefficient , writing ; comparison of the two forms gives . The form is the one used in the open-source pooltool simulator, whose physics documentation [10] [C/S] is unusually explicit about the model and states plainly that is a phenomenological parameter standing in for the finite contact area.
Alciatore's compiled property table [11] [C] gives the empirical answer directly as a ball–cloth spin deceleration rate of 5–15 rad s⁻², which is the most directly usable number for an implementer.
Mathavan et al. [4] add an interesting empirical observation on this point: they never observed a ball continuing to spin about the vertical axis after its linear motion had stopped, and conclude that a billiard ball on cloth exhibits partly disk-like coupling between translation and rotation, citing Voyenli and Eriksen's analysis of the ice-hockey puck [12] [P]. Whether translational and spin motion genuinely terminate simultaneously for a billiard ball, as they provably do for a uniform disk, is not settled — Mathavan et al. offer it as an observation, not a proof.
Three distinct effects displace the cue ball from the line the player aimed along, and they are routinely confused. Alciatore [2] separates them cleanly:
Squirt (cue-ball deflection). When the tip strikes off the vertical centre line, the cue ball leaves not along the cue's axis but deflected away from the side the tip struck. The mechanism is not friction: it is that the end of the cue shaft has mass, and as the ball rotates under the tip during the ~1.5 ms of contact, the shaft end must be pushed sideways; the reaction pushes the ball the other way. The governing quantity is the cue's effective endmass — how much of the shaft's mass is brought into play before the transverse elastic wave has travelled away from the tip. Shepard derives the relation [13] [C]; Cross measured it experimentally with the cue ball suspended as a pendulum bob [14] [P]. Alciatore's endmass experiments [2] found that adding mass to the shaft beyond about 18 cm from the tip had no effect on squirt, which is a direct measurement of the wave-transit length. Squirt angle is very close to linear in tip offset. Kim [15] [P] gives an independent theoretical treatment of the cue–ball collision and derives the squirt angle for an oblique strike.
Cross has continued this line, and the later papers matter for a VR engine because they concern where a stroke's energy actually goes. Cross [14] measured that an unchalked tip slides across the ball, producing a large squirt angle, whereas a chalked tip grips it, producing a smaller one. Cross [133] [P] then found that a large fraction of the energy loss in a stroke occurs in the tip rather than in the cue, and — significantly for anyone modelling a hand-held implement — that the player's hand alters both the cue's effective mass and the coefficient of restitution. Cross [134] [P] relates outgoing speed and spin to the impact point above or below centre and compares the measurements with a simple impact model. Shimamura et al. [135] [P] examine how the cue's material properties affect the resulting ball trajectory.
Swerve. With the cue elevated, the tip imparts a spin component about the aiming-line axis ("massé spin"). This spin puts a slip velocity at the contact perpendicular to the direction of travel, so the constant sliding-friction force of §1.3 has a lateral component, and the ball follows a parabola — curving back toward the aim line, or past it, depending on the sign. Swerve is a cloth effect and grows with the time and distance available before the object ball; squirt is a tip effect and happens instantly. A player compensating for one is not compensating for the other.
Massé. With extreme elevation the same mechanism is pushed to its limit and the cue ball describes a pronounced arc. Coriolis' Chapter VIII treats this, and his aiming construction (§1.1, result 3) is a geometric restatement of the parabola. Walker's Scientific American article [16] [C] is the standard popular exposition of follow, draw and massé and is cited as such by both Mathavan et al. [4] and Alciatore [2].
An important and often-missed point from Mathavan et al. [17] is that the massé mechanism also operates after every ball–ball collision: friction at the ball–ball contact, acting on a cue ball that has topspin, imparts a spin component about the cue ball's own forward velocity axis, producing exactly a small massé and hence a curved path. Their 2014 paper adds this correction to the post-collision trajectory prediction.
This is where an implementer needs to be careful, because the published numbers disagree by factors of two, and some of the disagreement is real (different cloth, different balls, snooker vs. pool) while some of it is measurement error in the older sources.
| Quantity | Value | Source | Type | Notes |
|---|---|---|---|---|
| Ball diameter, pool | 2.25 in = 57.15 mm (+0.005 in) | WPA equipment spec [18] | [C] standards body | |
| Ball mass, pool | 5.5–6 oz = 156–170 g | WPA [18]; Shepard [6] | [C] | |
| Ball radius/mass, snooker | R = 26.25 mm, M = 0.1406 kg | Mathavan et al. [5,17] | [P] | the values used in their simulations |
| Ball diameter, snooker | 52.5 ± 0.05 mm | WPBSA rulebook [49] | [C] standards body | Shepard [6] gives 2⅛ or 2 1/16 in for older sets; Mathavan et al. measured 52.4 mm [4] |
| Ball weight tolerance, snooker | ≤3 g spread within a set | WPBSA [49] | [C] | no absolute mass is specified |
| Ball diameter, carom | 2 27/64, 2⅜ or 2 7/16 in; 7–7.5 oz | Shepard [6] | [C] | carom balls are heavier |
| Moment of inertia | exact for a homogeneous sphere | — | ||
| Ball–cloth sliding friction | 0.178–0.245 (snooker, measured) | Mathavan et al. [4] | [P] | deceleration 1.75–2.40 m s⁻²; they use 0.21 as the average [5,17] |
| 0.2 (pool, computed) | Marlow [19] | [B] | ||
| 0.14 rising to 0.21 with speed | Witters & Duymelinck [20] | [P] | stroboscopic photography; Mathavan et al. could not reproduce this speed dependence [4] | |
| 0.15–0.4, typical 0.2 | Alciatore [11] | [C] | compiled range | |
| Ball–cloth rolling resistance | 0.0127–0.0129 (snooker, measured) | Mathavan et al. [4] | [P] | deceleration 0.124–0.126 m s⁻² |
| 0.011–0.024, mean 0.016 (pool) | Marlow [19] | [B] | Mathavan et al. explicitly criticise this >100% spread as a metre-stick-and-stopwatch artefact [4] | |
| 0.005–0.015 | Alciatore [11] | [C] | ||
| ~0.007–0.020 (as ) | Shepard [6] | [C] | ||
| Ball–cloth spin decay | 5–15 rad s⁻² | Alciatore [11] | [C] | |
| Ball–ball friction | 0.05 (best fit to high-speed data) | Mathavan et al. [17] | [P] | fitted over 0–0.2 in 0.01 steps |
| 0.06 (polished); up to 0.2 with chalk | Marlow [19] | [B] | ||
| Marlow [19] | [B] | speed-dependent; = slip speed. Mathavan et al. judge the experiment behind it unreliable [17]; Alciatore uses the same functional form fitted to his own and Marlow's data [2] | ||
| 0.03–0.08 | Alciatore [11] | [C] | ||
| Ball–ball restitution | 0.89 (best fit) | Mathavan et al. [17] | [P] | |
| >0.92 | Marlow [19] | [B] | ||
| ~0.93 | Alciatore [2] | [C] | quoted as the reason the "90° rule" is really an 85° rule | |
| 0.92–0.98 | Alciatore [11] | [C] | ||
| Ball–cushion restitution | 0.98 (true, from the impulse model) | Mathavan et al. [5] | [P] | this is the energetic COR inside the 3D model |
| 0.818 (effective, ball rolling, normal incidence, 0.28–3.5 m/s) | Mathavan et al. [4] | [P] | lumps in friction and 3-D effects; not the same quantity as 0.98 | |
| 0.910 (low-speed limit, rigid cushion) | Mathavan et al. [4] | [P] | slope of the rebound curve at low speed | |
| 0.55 (pool) | Marlow [19] | [B] | Mathavan et al. argue this measures the speed after the post-impact slide, not the impulse [4] | |
| 0.6–0.9 | Alciatore [11] | [C] | ||
| Ball–cushion friction | 0.14 | Mathavan et al. [5] | [P] | |
| Ball–table restitution | 0.5–0.7 | Alciatore [11] | [C] | for a ball dropped/hopping |
| Tip–ball friction | ~0.6 (0.58 at 0.5R, 0.66 at 0.55R offset) | Alciatore [2,11] | [C] | sets the miscue limit |
| Tip–ball restitution | 0.71–0.75 (leather); 0.81–0.87 (phenolic) | Alciatore [11] | [C] |
Reading this table. The single most useful observation is that the two best-instrumented studies — Mathavan et al.'s high-speed vision work [4] and Alciatore's high-speed video and robot work [2] — agree with each other far better than either agrees with the older stopwatch-era numbers. Where Marlow [19] and the modern measurements disagree (rolling resistance, cushion restitution), there is a specific, stated methodological reason to prefer the modern number. Marlow's book remains valuable for the forms of the relations, especially the exponential speed-dependence of , which Alciatore independently re-fitted and found to describe his data well [2].
The second observation is that "coefficient of restitution" for a cushion is three different quantities depending on what you measure (§3.4), and quoting 0.98, 0.91, 0.82 and 0.55 as though they were competing estimates of one number is a mistake that appears repeatedly in secondary sources.
Two equal-mass spheres, frictionless and perfectly elastic, one initially at rest. Let be the unit vector along the line of centres at contact ("impact line") and perpendicular to it in the table plane ("tangent line"). A frictionless impulse can only act along , so the cue ball's tangential component is untouched:
Along , momentum and energy conservation for equal masses give the classic one-dimensional exchange: the balls swap normal velocities, , . Hence the cue ball leaves purely along and the object ball purely along : the outgoing paths are perpendicular, whatever the cut angle. If is the cut angle (between the cue ball's incoming line and the object ball's outgoing line),
Two immediate corollaries. At the cue ball stops dead (the stop shot). And the ghost-ball aiming construction follows directly: to send the object ball along a chosen line, place an imaginary cue ball tangent to the object ball on the far side of that line, and aim at its centre. Alciatore [2] gives the terminology and the construction.
The 90° rule is exact only for (i) equal masses, (ii) a perfectly elastic collision, and (iii) a cue ball with no vertical-plane spin at contact — i.e. a stun shot. Condition (ii) fails: with the cue ball retains a little impact-line momentum, so the separation angle is nearer 85° [2, TP A.5]. Condition (i) fails on coin-operated tables with an oversized cue ball, and fails slightly everywhere because the cue ball wears fastest [2]. Condition (iii) fails on most shots, which is the subject of §2.3.
The standard parameterisation, used by Wallace and Schroeder [21] [P] and repeated by Mathavan et al. [4], is the fractional impact parameter
where is the ball diameter and is the perpendicular offset of the ball centres from the incoming velocity line. A half-ball hit () corresponds to ; a quarter-ball hit to ; a three-quarter-ball hit to .
Most shots reach the object ball with the cue ball in natural roll, so condition (iii) fails. What happens is a two-stage process, and the second stage is on the cloth, not in the collision:
The net deflection of the cue ball from its original aiming line, after the post-impact slide has finished, is (Wallace and Schroeder [21], quoted in this form by Mathavan et al. [4] eq. 2 and by Alciatore [2] eq. 1):
with the corresponding post-slide speeds
The in the denominator is the ball's dimensionless moment of inertia and the prefactors are the natural-roll factor of §1.3 — this is a cloth result wearing a collision's clothing.
Recovering Coriolis' 33.7°. It is worth doing this maximisation, because it shows that Coriolis' fourth conclusion and the Wallace–Schroeder formula are the same result. Write and use the double-angle identities:
Setting and differentiating, the numerator of is , so the maximum is at
These are exactly Coriolis' numbers. (I verified this numerically as well as analytically.) It is a pleasing check on 190 years of transcription.
The practically important part is not the peak value but that is flat near its maximum: it stays close to 30° for ball-hit fractions from ¼ to ¾ [2]. That flatness is the entire content of the "30° rule" and the reason it survives as a practitioner heuristic — a player can be substantially wrong about the cut angle and still predict the cue ball's line.
Onoda [22] [P] published a comment on Wallace and Schroeder in AJP the following year, and both Alciatore [2] and Mathavan et al. [4] cite Wallace-and-Schroeder-plus-Onoda together as the supporting analysis. Wallace and Schroeder's own experimental validation was incomplete: their tracking method recorded positions but not times, so they could not check the velocity relations. Mathavan et al. [4] supplied that check with high-speed video, finding agreement within about 10% on post-collision speeds (their Table I), with the largest disagreement for at large impact parameters.
Throw is the deviation of the object ball from the pure impact-line direction, and it is caused by tangential friction at the ball–ball contact. There are two sources, and Alciatore's naming for them [2] is now standard:
Alciatore's model [2, TP A.14] gives the throw angle as
Two features of this expression are worth understanding because they explain the observed behaviour and because a simulator that omits either will be visibly wrong:
The exponential friction model. parameterise a that decreases with relative surface speed, following Marlow's form [19] refitted by Alciatore. This is why fast shots throw less than slow ones at large cut angles.
The 1/7 cap. The min enforces that the relative sliding at the contact cannot reverse during the impact. Once the surfaces stop slipping relative to each other, friction can do no more work, so there is a hard ceiling on the total tangential impulse. Deriving it: the tangential impulse changes the relative surface velocity by contributions from both balls' linear motion () and both balls' spin about the contact (), so ; setting gives the maximum useful impulse and hence the factor. Alciatore notes explicitly that prior treatments in the literature — he names Shepard's chapter 4 [6] — omitted this no-slip-reverse constraint.
The consequences, from Alciatore's Figure 20 [2], are: - at small cut angles throw is independent of speed (the 1/7 cap binds, so drops out entirely) and grows with cut angle; - at larger cut angles throw is strongly speed-dependent (slow shots throw much more); - the maximum throw occurs near a half-ball hit, .
Experimental confirmation comes from Jewett's measurements in Billiards Digest [23] [C] — Alciatore reproduces Jewett's data as his Figure 21 — and from Alciatore's own two-part throw study [24,25] [C]. This is an area where the practitioner literature carries the experimental weight; there is no refereed paper presenting a comparable throw dataset.
Four bodies of work go beyond the algebraic treatments above and integrate the collision as a process:
Mathavan, Jackson and Parkin (2014) [17] [P] derive three sets of twelve differential equations for the two-ball collision on a rough surface, using the normal impulse rather than time as the independent variable — the standard device from Stronge's Impact Mechanics [26] [B] — and integrate numerically, tracking whether each ball is rolling, sliding or airborne at each of the three contacts (ball–ball, and each ball with the cloth). They fit , against their own high-speed data, and report that their method roughly halves the error in post-collision speed and doubles the accuracy of predicted travel direction relative to the Wallace–Schroeder algebraic prediction. They also derive the post-impact parabolic path arising from the induced frontal-axis spin (§1.6).
Doménech has a long series on non-smooth collision modelling, culminating in "Non-smooth modelling of billiard- and superbilliard-ball collisions", International Journal of Mechanical Sciences 50(4):752–763, 2008 [27] [P], which treats the collision as a two-step discontinuous process (ball–ball interaction, then ball–surface interaction) for a cue ball with arbitrary pivot and sidespin. He has continued the theme with an "independent friction–restitution" framework, e.g. Mechanics Research Communications, 2023 [28] [P] — I confirmed this article exists and is by A. Doménech-Carbó, but was unable to retrieve the full record, so the volume and page numbers should be checked before citing.
Peskin (2020) [29] [R] gives a compact, self-contained centre-of-mass treatment of the general 3-D collision of two identical spinning balls under Coulomb friction. His framing is the most directly implementable of the set: because the balls are identical and the frame is the centre of mass, ball B's state is always the negation of ball A's, halving the problem. He shows that when sliding friction is the only dissipative mechanism the outgoing state is determined unambiguously by the incoming state, and that adding compression-phase dissipation introduces a one-parameter family of solutions. That last point is the honest statement of why "coefficient of restitution" is not a single well-defined number for a frictional collision — a point simulator authors often discover the hard way.
Kim (2024) [30] [P] analyses the collision of two spinning balls including all three deviations from the ideal — inelasticity, ball–ball friction and ball–table friction — and concludes that friction "significantly influences the post-collision motions". Its companion [15] does the same for the cue–ball impact. Both are single-authored and both, despite circulating widely as arXiv preprints, are published in New Physics: Sae Mulli.
Note the pattern: every one of these treats the collision by integrating over the normal impulse with the contact state (stick/slip) re-evaluated at each step. That is the correct structure for a high-fidelity ball–ball model, and it is not what most game physics engines do (§5).
This is the hardest part of billiard physics and the part where a simulator most obviously succeeds or fails, because banking is where players' expectations are sharpest.
The naive model reflects the velocity about the cushion normal and scales the normal component by a restitution coefficient: outgoing normal, tangential. Mathavan et al. [5] call this "current theory" and list what it omits. Three things are wrong with it.
(a) The contact point is above the ball's equator. The cushion profile is cut so that the ball touches it above the horizontal great circle. The nominal reason is to stop the ball leaping into the air on rebound, and the geometry is unavoidable: the cushion has to be high enough to keep the ball on the table. The consequence is that the contact normal is tilted downward by an angle from horizontal, with
so the impulse has a vertical component and produces a torque about a horizontal axis — the impact necessarily changes the ball's topspin/backspin state and presses it into the cloth. Mathavan et al. take and hence [5].
(b) There are three simultaneous contacts, not one. During the impact the ball is in contact with the cushion and with the cloth. Mathavan et al. [5] carry both: friction at the cushion (coefficient ) and friction at the table (coefficient ), with the table's normal reaction itself a function of the cushion impulse. This coupling is why the ball–cushion problem is genuinely three-dimensional even for a shot struck flat.
(c) The slip direction at the cushion is not constant. Unlike the cloth case of §1.3, where is provably fixed, the slip direction at the cushion rotates during the impact. Mathavan et al. plot the slip–impulse curves and state directly: "the assumption of unidirectional slip cannot be true" [5, Fig. 12]. Any closed-form cushion model that assumes a fixed friction direction — including Marlow's [19], which they criticise on exactly this ground — is approximating.
This deserves its own subsection because the two numbers in circulation are not the same and the difference matters.
Mathavan et al. [5] state "In both snooker and pool ", and in their 2009 paper [4] they measure a snooker cushion height of 36 mm against a 26 mm ball radius and describe this as "close to the height of 1.4 times ball radius found in pool". 36/26.25 = 1.37, which is close to 1.4 — but the WPA pool figure of 1.27R is not.
This is a genuine inconsistency in the literature that a careful paper should flag. I checked the WPBSA Official Rules of the Games of Snooker and English Billiards (May 2022) directly: it specifies the playing area, the table height from the floor to the top of the cushion rail (864 mm), the ball diameter (52.5 ± 0.05 mm, hence exactly Mathavan's mm), and the ball-weight tolerance — but it does not specify the cushion nose height above the bed at all. So the 1.37–1.4R figure for snooker rests on Mathavan et al.'s single measurement of one Riley Renaissance table, not on a standard, while the 1.27R figure for pool is a written WPA specification.
Possible explanations for the gap: snooker and pool cushion profiles genuinely differ; the measurement datum differs (nose apex versus the point at which a ball of that radius actually touches a curved nose profile — these are not the same point, and for a convex nose the contact point sits below the apex); or one source is in error. I could not resolve it and recommend measuring directly before adopting either value. The sensitivity is not small: is at but only at — a one-third change in the vertical component of the cushion impulse, and hence in how much topspin the cushion converts.
The two papers should be read as a pair. The 2009 AJP paper [4] [P] is the experiment; the 2010 IMechE paper [5] [P] is the theory.
Experiment [4]. A single PixeLINK PL-B776F machine-vision camera (3.15 Mpx, up to 1000 fps in ROI mode) mounted vertically above a Riley Renaissance 10×5 ft snooker table, calibrated intrinsically (Bouguet's MATLAB toolbox) and extrinsically to give 1 mm spatial resolution over the table, verified against metre-stick measurements to within 2 mm. Ball centroids extracted per frame; the speed–time plot is the key analytical device, because the change of gradient identifies the sliding-to-rolling transition unambiguously. They report the friction coefficients of §1.7, and for the cushion they restricted themselves to shots played perpendicular to the rail with no sidespin (verified by checking the ball returned along its own path), leaving top/back spin as the only unknown.
Theory [5]. A full 3-D impulse analysis. The independent variable is the accumulated normal impulse at the cushion, which is monotonic in time and therefore a legitimate substitute for it [26]. Forces are resolved at the cushion contact and the table contact ; friction impulses follow Amontons–Coulomb in the instantaneous slip direction at the cushion and at the table, both of which are recomputed at every step. Restitution is imposed through Stronge's energetic coefficient, — the ratio of work done during restitution to work done during compression — rather than a kinematic velocity ratio, precisely because the kinematic definition is not friction-independent. Integration proceeds in ~5000 steps of .
Calibration. By matching the numerical rebound speeds to the measured perpendicular-incidence data over m/s, scanning and over in steps of 0.01, they obtain
with for the table contact, kg, mm.
Validity limit. Numerical and experimental rebound speeds agree up to a normal incident velocity of 2.5 m/s, beyond which they diverge — this is where the rigid-cushion assumption fails and the rubber begins to deform substantially and the ball to "sink" into it [5]. For a simulator this is an important and rarely quoted number: below ~2.5 m/s normal component, a rigid-cushion impulse model is defensible; above it, it is not.
Predictions. From their Figures 8–10: - Rebound angle for a rolling ball with no sidespin is independent of incident speed and depends only on incident angle. (Speed affects the rebound speed, not the angle, in this case.) - Speed loss is greatest for incident angles around 40°. - Overspin raises the rebound speed; at and normal incidence the rebound speed equals the incident speed. Excess topspin has little effect on the rebound angle. - Sidespin has a large effect on both. With "right" spin the rebound speed can exceed the incident speed (energy coming out of the spin reservoir). With strong left spin at incident angles near 90°, the ball rebounds back to the side it came from — an effect described qualitatively in the billiards literature by Walker [16] and analysed in a general ball-bounce context by Cross [32] [P], whose tennis-ball measurements near normal incidence show the same phenomenon.
Acknowledged limitation. The model is not validated against measured spin, only against speeds and angles, because their vision system could not track ball rotation. They recommend a coloured pattern on a white cue ball for future work. As far as I can determine, that validation has not been published, which means the cushion model's spin predictions remain theoretically derived but experimentally unconfirmed.
Mathavan et al.'s own numbers illustrate the trap. For perpendicular incidence with a rolling ball over 0.28–3.5 m/s [4]:
These are not competing measurements. 0.818 is a lumped, phenomenological number that includes frictional losses at both contacts and the redistribution of energy into spin; 0.98 is the restitution of the rubber alone once friction has been separated out. The fall-off at higher speeds is attributed to cushion deformation, i.e. to the breakdown of the rigid assumption, not to a genuine velocity-dependent material property [4].
A simulator using a single scalar cushion restitution is therefore implicitly choosing the lumped number, and should expect to be wrong about spin.
The practitioner literature encodes the same physics as rules of thumb. These are worth knowing because they are what players expect a simulator to reproduce, and because they constitute a free (if coarse) validation suite.
The mirror / equal-angle system. Sight the mirror image of the target through the rail; the crossing point is the aim point [33] [C]. It is a first-order approximation only.
The 70% shallow-angle rule. For kicks nearly parallel to a cushion, Bob Jewett states that "the cue ball comes off the cushion at only 70% of the slope going in if the cue ball is rolling" — i.e. the rebound angle to the cushion is 70% of the incoming angle, not 100% as the mirror predicts, and this holds even with reverse sidespin. Alciatore documents the resulting aiming system [33] [C], which is the mirror system corrected by that factor. Provenance warning: Alciatore's page attributes the statement to a Jewett forum post rather than to a Billiards Digest column; I was unable to locate a primary published measurement behind the 70% figure, so it should be cited as a widely-used practitioner heuristic and not as a measured coefficient. Its physical content is real, though, and consistent with §3.3: at shallow incidence the cushion-parallel component is attenuated by friction while the (small) perpendicular component is largely returned, and a rolling ball's topspin is converted at the cushion in a way that pulls the rebound short.
The diamond system for three-cushion carom is the most elaborate practitioner construction, and unusually it has a peer-reviewed treatment: Cohen, G. L., "Three cushion billiards: notes on the diamond system", Sports Engineering 5(1):43–51, 2002 [132] [P]. A clear, freely available mathematics-outreach exposition is Polster and Ross [34] [C].
Documented qualitative effects [35] [C], all consistent with §3.3: - topspin/follow lengthens the rebound; draw shortens it; - running (natural) english lengthens and speeds up the rebound; reverse english shortens and slows it; - sidespin has minimal effect at shallow angles and maximum effect on a ball driven straight into the rail; - faster shots rebound shorter for a rolling cue ball, because the post-rebound curve is delayed; - the cushion imparts and can reverse spin on the rebounding ball, so transferred sidespin from a cut affects the bank path (outside cuts shorten banks, inside cuts lengthen them); - new, slick cloth and clean balls give longer banks; a bouncier cushion (higher COR) gives shorter rebounds.
That last item is counter-intuitive and is a good sanity check for a simulator: raising cushion restitution should shorten the bank, because it increases the perpendicular component relative to the parallel one.
The cushion is the least-settled part of the classical problem. Specifically open: the nose-height discrepancy (§3.2); the absence of any spin-validated cushion experiment; the behaviour above 2.5 m/s normal velocity, where the rigid-cushion model is known to fail and no published deformable-cushion model exists; and the effect of cloth condition and cushion temperature, which practitioners regard as first-order and for which there is no quantitative literature at all.
This section is short because the literature is genuinely thin. There is no peer-reviewed treatment of pocket capture dynamics that I could locate. What exists is (a) a standards specification, (b) careful practitioner measurement, and (c) one recent preprint on a tangential question.
The terms, from the WPA specification [18] [C] and its commentary:
A striking observation, and a caution for anyone modelling snooker specifically: the WPBSA Official Rules [49] [C] contain no numerical pocket dimensions whatsoever. Rule 1(g) reads, in its entirety, "There shall be a pocket at each of the four corners of the table and one each at the middle of the longer sides." Snooker pocket geometry is fixed by separate manufacturer templates rather than by the rules of the game, whereas pool pocket geometry is specified numerically by the WPA. A simulator that wants to be right about snooker pockets must therefore work from a physical template, not from a rulebook.
Practitioner analysis treats pocket difficulty as the product of three factors — mouth width × facing angle × shelf depth — rather than mouth width alone [36] [C], and the shelf is argued to be the most underrated of the three, since a ball entering with too much speed and too little downward momentum can cross the shelf and hit the far jaw.
The most useful data I found are an explicitly experimental practitioner study by Bob Jewett, with Alciatore, reported in Billiards Digest [37] [C]. Their method: a four-ball combination set on ruled paper (four balls so that essentially no spin is transferred to the last ball), aligned to a thread whose angle to the cushion was measured by offsets at two known distances; the paper was translated laterally until the pocketing success rate was 50%, defining the "edge" of the pocket; the difference between the two edges is the effective width at that angle. Two speeds were used, "soft" (ball rolling smoothly) and "firm" (partly sliding).
Their findings, which contradicted both of their own prior theories:
On effective pocket centre, Alciatore's summary [38] [C] reports that the best aim point shifts from the geometric pocket centre by up to about 0.6 in for corner pockets and 0.3 in for side pockets depending on entry angle. Jewett's earlier hypothesis — that the single best target is the intersection of the "rail grooves", the worn lines half a ball out from each cushion nose — turned out to be only approximately right [37].
I found no peer-reviewed model of rattling (multiple jaw bounces) or of capture probability. The nearest published items are:
This is a real gap. For a VR engine the practical consequence is that pocket behaviour must be tuned against practitioner data (§4.2) and against the geometry, because there is no model to implement.
(Sections 5.1–5.7 follow; see the continuation below.)
The defining observation is the one derived in §1.3: during any one motion phase, a billiard ball's acceleration is constant. While sliding, the friction force is fixed in magnitude and direction; while rolling, the rolling resistance is a constant deceleration antiparallel to the (fixed-direction) velocity. Therefore, within a phase,
a quadratic in with known coefficients. Everything follows from that.
Transition times are linear. Slide ends when reaches zero, at ; roll ends when reaches zero, at ; vertical-axis spin ends at . All closed form.
Ball–ball collision times are quartic. Two balls collide when . Squaring, and with both quadratic in , the difference is quadratic and its squared magnitude is a quartic polynomial in . Quartics have closed-form roots (Ferrari), so no root-finding iteration is required.
Ball–cushion collision times are at most quadratic, from the ball centre reaching a line offset from the cushion nose.
Will Leckie and Michael Greenspan turned this into a complete simulator. The work appears in three places:
Their stated contribution is that "the occurrences of all possible events are determined as the roots of polynomials up to fourth order, for which closed-form solutions exist", giving a method that is "both accurate, returning continuous space solutions for both time and space parameters, and efficient, requiring no iterative numerical methods" — and therefore suitable both for game-tree search (where the same shot is evaluated thousands of times) and for robotic pool (where the prediction must be trustworthy). The main loop is: compute every candidate event time, take the earliest, advance every ball analytically to that instant, resolve the event, repeat.
Trade-offs versus time-stepped simulation.
| Event-driven | Fixed-timestep rigid body | |
|---|---|---|
| Accuracy within a phase | Exact (to the model) | or integration error |
| Tunnelling | Impossible — collisions are solved for, not detected | Possible; needs CCD or small steps |
| Determinism / reproducibility | Excellent; the same shot replays bit-identically given the same root solver | Good but sensitive to step boundaries and solver iteration order |
| Cost | Proportional to the number of events, not to elapsed time. Very cheap for a settling table | Proportional to elapsed time; a slowly-settling table costs as much as a break |
| Simultaneous / persistent contacts | Poor. Resting contact, a cluster of frozen balls, a ball leaning in a pocket jaw all produce event storms | Handled naturally by the constraint solver |
| Extending the model | Every new effect needs a new analytic solution | Add a force; the integrator copes |
| Non-sphere geometry, arbitrary meshes | Impractical | Routine |
| Coupling to other physics (cue stick as a rigid body, a ball leaving the table) | Awkward | Natural |
The last three rows are the reason a VR engine cannot simply adopt the event-driven approach wholesale: a VR cue is a tracked rigid body moving at arbitrary speed with arbitrary orientation, the player can reach in and knock balls about, and objects leave the table. The event-driven method is at its best in exactly the setting Leckie and Greenspan built it for — a closed system of spheres on a plane with a scripted stroke.
Zeno / event-storm behaviour is the specific failure mode. As a ball settles against a cushion or a jaw, the interval between events collapses toward zero and the simulation can stall. Event-driven billiard simulators handle this with velocity and time thresholds — the analogue of the sleeping criteria discussed in §5.6.
Two open-source implementations document the approach in usable detail:
Kiefl's cushion choice is worth noting: pooltool implements Han's model, not Mathavan's. Han, I., "Dynamics in carom and three cushion billiards", Journal of Mechanical Science and Technology 19(4):976–984, 2005 [46] [P], DOI 10.1007/BF02919180, treats all three problems — motion on the bed, ball–ball collision and ball–cushion collision — within rigid-body mechanics, with a numerical simulation program. Han's cushion model is instantaneous and assumes negligible cushion deformation, parameterised by the contact height above the ball centre, giving impact angle — structurally the same idealisation as Mathavan et al.'s [5] but resolved algebraically rather than by integrating over the impulse.
Further implementations worth knowing are Zitelli's PoolPhysics [47] [S], a Python event-based simulator on GitHub; Poolimmersion [48] [P] — Silva, Malhadas and Ramires Fernandes, EPCGI 2017 — a graphics-conference paper pairing a physically-based simulation with a simulated-annealing artificial player benchmarked against computational-billiards competition winners; and the GPL projects Billiards [129] and Foobillard++ [130] (§7.4). A caution about the FastFiz link above: it is a rescue archive. The original code vanished from Stanford's billiards site and was recovered from the Internet Archive, and its original authorship and licence are not stated — which is itself a small case study in how fragile this field's software record is.
The event-driven and impulse-based approaches of §5.1 and §§2.5 and 3.3 all share an assumption: the collision is instantaneous, so the contact is resolved as a jump in velocity and the geometry does not change during it. The alternative is to treat contact as a continuous force that is a function of penetration depth, integrate it with an ordinary ODE solver, and let the collision take real time. This is the "smooth" or "penalty" or "compliant contact" family, and it has been applied to billiards seriously and recently.
The foundational contact law is Hunt, K. H. and Crossley, F. R. E., "Coefficient of Restitution Interpreted as Damping in Vibroimpact", Journal of Applied Mechanics 42(2):440–445, 1975 [50] [P], DOI 10.1115/1.3423596. Their insight was that the usual linear (Kelvin–Voigt) spring–damper contact is physically wrong at the moments that matter: it produces a discontinuous force at first touch (damping force is finite when ) and a tensile force at separation, sticking the bodies together. Hunt and Crossley instead made the damping term proportional to the penetration as well as its rate,
so the force vanishes smoothly at both the beginning and the end of contact, and the hysteresis loop in the – plane has physically sensible area. This is the standard reference for compliant contact and is the ancestor of essentially every modern penalty formulation. (A 50th-anniversary retrospective article appears to exist in Mechanism and Machine Theory, 2025, but I exhausted my search budget before confirming its authorship, so it is not listed in the references.)
The billiards application is Gismeros Moreno, R., Corral Abad, E., Meneses Alonso, J., Gómez García, M. J. and Castejón Sisamón, C., "Modelling multiple-simultaneous impact problems with a nonlinear smooth approach: pool/billiard application", Nonlinear Dynamics 107(3):1859–1886, 2022 (online 2021) [52] [P], DOI 10.1007/s11071-021-07117-4. Its argument, and its choice of billiards as the test case, are directly relevant here: the paper is about multiple simultaneous impacts — the case where a cluster of balls is struck and several contacts are live at once, which is exactly where the instantaneous-impulse formulations become ambiguous (there is no unique answer to "which contact resolves first"). Billiards is chosen precisely because the geometry is simple enough that the contact question is isolated.
Their model, as far as I could verify it:
| Contact | Normal force | Friction |
|---|---|---|
| Ball–ball | Lankarani–Nikravesh [165] [P] (a Hunt–Crossley-family law with hysteresis damping), , , | Constant (attributed in the paper to Threlfall — a name I could not independently verify), or the velocity-dependent exponential form — this is Marlow's fit [19] (§1.7) |
| Ball–cushion | Lankarani–Nikravesh, , taken from Mathavan et al. [5] | Ambrósio regularised friction, , again from Mathavan et al. [5] |
| Ball–cloth | Plain Hertz, , no damping | Ambrósio regularised friction with tolerance velocities m/s, m/s |
The "Ambrósio" friction model is the standard regularisation used in multibody dynamics: the Coulomb force is ramped smoothly from zero to as the tangential slip speed rises through to , which removes the discontinuity at zero slip that makes stiction impossible to integrate. The two tolerance velocities are the numerical price of smoothness: below the model has no friction at all, so a genuinely static ball is held by nothing.
They validate against three independent things: Alciatore's 30° rule [2] across a range of impact fractions; high-speed video of ball–cushion impacts; and Mathavan et al.'s [4] measured restitution and friction for normal cushion impacts.
Trade-offs against the impulse route.
In favour of compliant contact: multiple simultaneous contacts are handled without arbitration, because every contact simply contributes a force; the contact process is resolved, so quantities that depend on it (the exact slip history, the induced spin) come out of the integration rather than having to be assumed; and it composes naturally with everything else in a general physics engine.
Against: the contact stiffness of a phenolic ball is enormous, so the contact duration is on the order of s and the ODE is very stiff. Resolving it demands either a tiny fixed timestep (catastrophic for a 90 Hz VR budget) or an adaptive/implicit integrator (which destroys determinism across machines). Parameters must be chosen for numerical behaviour rather than measured, and the effective restitution then depends on the integrator. This is the reason real-time engines almost universally use impulses instead: an impulse method makes the contact duration a modelling assumption rather than a computational cost.
The honest summary for a VR engine is that compliant contact is the more physically complete formulation and the right one for offline study, and the impulse formulation is the only one that fits a real-time frame budget — so the design question is how much of the compliant model's behaviour can be recovered by an impulse method that is applied carefully (§§5.4–5.8).
Mathavan et al. [5] state that their model is valid only below a normal incident velocity of 2.5 m/s, above which the rubber deforms enough that the rigid-cushion assumption fails — and 2.5 m/s is not a hard shot. Above that threshold there is, as far as I can determine, nothing. I searched specifically for finite-element or hyperelastic modelling of a billiards cushion profile and found none. The comparison is instructive: table tennis rubber has a real FEM/hyperelastic literature, and so do golf balls, tennis balls and footballs, but the billiards cushion — a Mooney–Rivlin-ish gum rubber extrusion with a specific and standardised profile, struck millions of times a year in televised competition — appears never to have been modelled as a deformable continuum in the published literature.
That is a genuine and rather surprising hole, and it is exactly where the practitioner community's strongest claims live: that cushion behaviour varies with rubber age, temperature and humidity, and that different table brands bank measurably differently [35]. None of that is quantified anywhere I can find. A paper that wants to state an open problem in cue-sports physics could not pick a better one.
Between the analytic extreme (§5.1) and the compliant extreme (§5.2) sits the approach every general-purpose real-time engine actually uses: treat contacts as constraints, solve for the constraint forces or impulses that satisfy them, and integrate.
What "analytical" means, and what it is set against. Baraff introduced this framing to graphics, and his 1989 paper is explicit that the alternatives available at the time were unsatisfactory: earlier work either modelled resting contact as "a series of frequently occurring collisions", or applied "arbitrary penalty forces that acted to separate penetrating bodies" — which he dismisses on the ground that "dynamical correctness of these forces was not a focus". He also notes that classical engineering methods do not transfer, because "these techniques assume that the systems of bodies being analyzed are in equilibrium", which a falling stack is not. Analytical therefore means: solve for the forces that would actually arise under the physics, rather than approximating them with springs or with a train of collisions.
The frictionless problem. At each contact, the normal acceleration is affine in the unknown force magnitudes, , subject to three conditions: contacts only push (); bodies do not accelerate into each other (); and the force is zero wherever the bodies separate. That last condition is the complementarity condition , and the system
is a linear complementarity problem (LCP), reached in Baraff, D., "Analytical methods for dynamic simulation of non-penetrating rigid bodies", ACM SIGGRAPH Computer Graphics 23(3):223–232, 1989 [152] [P] through a quadratic-programming framing. He is candid about the difficulty even here: "the term is quadratic in … Quadratic programming, unlike linear programming, is an NP-hard problem in general." For frictionless contact is positive semi-definite, so the QP is convex and polynomially solvable in principle, though he notes "no practical polynomial time algorithms are currently known", and warns that "there is no reason to believe that will remain PSD when friction is added."
With friction it breaks, in two distinct ways. Baraff, D., "Coping with Friction for Non-penetrating Rigid Body Simulation", ACM SIGGRAPH Computer Graphics 25(4):31–41, 1991 [153] [P], settles this. First, convexity is lost: frictionless configurations always admit a valid force set solvable as a convex QP, and although the forces need not be unique, "all valid contact forces yield the same accelerations"; with friction, "the QP associated with a configuration involving dynamic friction is not necessarily convex. The existence of a practical solution method for non-convex QP's is considered unlikely, because solving non-convex QP's is NP-hard." Second, existence is lost: "it has been known for at least a century that configurations of rigid bodies with dynamic friction exist that have no valid set of contact forces. We call such a configuration inconsistent." The dual failure is indeterminacy — several valid force sets giving different accelerations.
The formal result, by reduction from Subset-Sum, is that deciding frictional consistency is NP-complete, and Baraff closes the obvious escape route: even a solver restricted to consistent configurations cannot run in polynomial time unless P = NP. His resolution is to abandon the classical principle of constraints — which insists that constraints be met by non-impulsive forces wherever possible — and to allow impulses at inconsistent configurations, justified by a near-rigid picture in which "contact forces arise as a result of small elastic deformations."
A correction worth recording. It is common to attribute the Painlevé paradox to Baraff's 1991 paper. The paper never uses the word; it cites Löstedt, Erdmann, and Mason and Wang for the inconsistent example. The right citations for the paradox itself are Stewart's SIAM Review survey [169] [P], which ties it to "paradoxical results known at least since Painlevé [C. R. Acad. Sci. Paris, 121 (1895), pp. 112–115]" [171], Stewart's convergence paper [172] [P], and Champneys and Várkonyi's review [170] [P]. The latter also supplies two historical corrections: the effect was first found by Jellett, not Painlevé (who acknowledges this), and Painlevé's own 1895 example was a box on an inclined plane, not the rod. Its mechanism: with a large enough friction coefficient and sufficient normal–tangential coupling, "the effective normal force becomes attractive rather than repulsive", and the system can reach dynamic jam, where "the more it tries to lift off, the more it rotates and digs back down into the surface", resolvable only by an impact without collision — an impulsive jump with no approaching normal velocity.
The practical follow-up. Baraff, D., "Fast Contact Force Computation for Nonpenetrating Rigid Bodies", SIGGRAPH '94 [154] [P], drops the optimisation framing entirely — "by moving to an optimization problem… we necessarily lose sight of the original condition for each contact point. Because of this, we are solving a more general, and thus harder, problem" — and adapts Dantzig's pivoting algorithm for LCPs. It builds the solution one contact at a time, maintaining index sets (clamped, , ) and (not clamped, , ), and to admit contact it increases by "the smallest scalar such that increasing by causes either to reach zero, or some index to move between and " — which is pivoting. The cost is roughly three times a dense Gaussian elimination of the same size, and it extends to mixed static and dynamic friction. His stated motivation is accessibility: simple enough that "nonspecialists in numerical programming can implement them on their own."
Since 1989, 1991 and 1994 are all conference papers, the peer-reviewed journal version of the complexity results is worth citing alongside them: Baraff, D., "Issues in computing contact forces for non-penetrating rigid bodies", Algorithmica 10(2–4):292–352, 1993 [167] [P]. The PhD thesis is Baraff, D., Dynamic Simulation of Non-Penetrating Rigid Bodies, Cornell University TR 92-1275, March 1992 [155] [T], and the canonical tutorial treatment — the clearest statement anywhere of the colliding versus resting contact split — is Witkin, A. and Baraff, D., "Physically Based Modeling: Principles and Practice", SIGGRAPH '97 Course Notes [168] [C].
Two things follow for a cue-sports engine. First, this is a direct method: it computes the contact forces for the whole system at once, which is what you want when a rack is struck. Second, its cost is superlinear in the number of simultaneous contacts and it is not naturally parallel — which is why real-time engines went elsewhere (§5.6).
Time-stepping complementarity. The insight that dissolves Baraff's difficulty is to stop asking the instantaneous question. An event-driven scheme must locate each impact or transition instant, back the simulation up to it and resolve it — and in the Painlevé and Zeno situations there is either nothing well-defined to resolve or an unbounded number of events to resolve. A time-stepping scheme never isolates an instant: it takes a fixed step and solves one complementarity problem in impulses rather than forces, so impulsive and non-impulsive behaviour are described by the same equations. Stewart and Trinkle put it that the method "does not need to identify explicitly impulsive forces."
The foundational papers are Stewart, D. E. and Trinkle, J. C., "An implicit time-stepping scheme for rigid body dynamics with inelastic collisions and Coulomb friction", International Journal for Numerical Methods in Engineering 39(15):2673–2691, 1996 [156] [P], and Anitescu, M. and Potra, F. A., "Formulating Dynamic Multi-Rigid-Body Contact Problems with Friction as Solvable Linear Complementarity Problems", Nonlinear Dynamics 14(3):231–247, 1997 [157] [P]. The word solvable in the latter title is the point: existence becomes a theorem rather than a hope, because the LCP's block matrix is copositive, so a solution exists and Lemke's algorithm constructs it. The concrete difference between the two is where non-penetration is imposed — Anitescu and Potra replace the position-level linearised constraint with a velocity-level one, . Stewart defends the velocity level directly: a position-level condition "behaves as if it had a 'random' coefficient of restitution when impacts occur", because the effective restitution depends on where inside the step the contact happens.
Anitescu's later open-access presentation [174] [P] states the payoff plainly: "We do not perform collision detection. Instead, a noninterpenetration constraint is replaced by its linearization, which… guarantees geometrical constraint stabilization without the need to perform a reduction of the time step to detect new collision or stick-slip transition events." It also frames the field cleanly as "regularization approaches and hard constraint (complementarity) approaches", and shows the LCP scheme is the stiff limit of a regularised (i.e. compliant, §5.2) contact model — which is a satisfying unification of the two families. The mathematical-programming counterpart from the same year is Pang, J.-S. and Trinkle, J. C., Mathematical Programming 73(2):199–226, 1996 [173] [P].
Impulse-based simulation. Mirtich and Canny, "Impulse-based simulation of rigid bodies", I3D '95, pp. 181–188 [158] [P], and Mirtich's Berkeley thesis (1996), invert the picture: there are no contact forces at all. "All contact interactions between bodies are affected through collisions; rolling, sliding, resting, and colliding contact are all modeled in this way", via microcollisions — an infinitesimal collision time with Poisson's hypothesis and Coulomb friction. Its claimed advantages are simplicity, robustness, parallelisability, and efficiency for systems with frequently changing contact modes.
Its weakness is exactly the case a pool table presents. The clearest third-party statement is Guendelman, Bridson and Fedkiw [175] [P], who say their own scheme "correctly models difficult frictional effects without requiring microcollision approximations… (which also use an ad hoc velocity)", and explain the failure: a body at rest with non-zero restitution receives a spurious elastic bounce every step, so "objects sitting on the ground… vibrate as they are incorrectly subjected to a number of elastic collisions. Thus, many authors use ad hoc threshold velocities in an attempt to prune these cases." Under a microcollision model a resting body is never truly still; it buzzes at the threshold. For twenty-two balls at rest between shots, that is disqualifying.
The broad survey covering all of this, and the right single citation for a related-work paragraph, is Bender, Erleben and Trinkle, Computer Graphics Forum 33(1):246–270, 2014 [136] [P].
The method that displaced direct LCP solvers in games is sequential impulses, from Erin Catto. Its bibliographic status is worth stating precisely because it is often mis-cited: Catto, E., "Iterative Dynamics with Temporal Coherence", Game Developer Conference, 2005 [150] [C] — a GDC talk with an accompanying self-published ~20-page technical article, not peer-reviewed and with no DOI — followed by "Fast and Simple Physics using Sequential Impulses", GDC 2006 [150] [C]. Both are hosted at box2d.org/publications, which is the authority for venue and year.
Catto's 2006 framing is an explicit rejection of the alternatives: "Penalty method? Nope. Linear complementarity (LCP)? Nope. Joint coordinates (Featherstone)? Nope. Particles (Jakobsen)? Nope. Impulses? Bingo!" His diagnosis of why naive impulses fail is that "impulses are good at making things bounce", so people get jitter, then "forget static friction" and "forget stacking." His method is five steps: accept penetration; remember the past (warm starting); apply impulses early and often (iterate); pursue the true impulse (accumulate and clamp the accumulator); update position last.
The normal impulse at a contact is
with the friction impulse the same expression along the tangent, clamped to .
The critical detail, and the one most reimplementations get wrong, is step 4. Clamping must be applied to the accumulated impulse and the increment recovered by subtraction:
temp = ΣPₙ
ΣPₙ = max(ΣPₙ + ΔPₙ, 0)
ΔPₙ = ΣPₙ − temp
with the same pattern for friction against . Catto states the failure mode of naive per-iteration clamping plainly: "each contact point forgets its impulse history", "each contact point requires that every impulse be positive", and "there is no way to recover from a bad impulse." Allowing negative increments on a non-negative accumulator is what lets the iteration converge to the true global solution instead of oscillating. The "temporal coherence" of the 2005 title is the complementary half: a contact cache keyed by contact-point identifier seeds each contact's multiplier from the previous frame, in time and space.
Sequential impulses is projected Gauss–Seidel, and three sources say so.
Two caveats Catto himself records. His 2005 convergence statement is honest: "According to Cottle, convergence is guaranteed if is positive definite. In many typical simulations is only positive semi-definite. Nevertheless, we have seen good visual and numerical results in practice." And his 2005 friction is decoupled from the normal force: "In Coulomb's friction law, the static and dynamic friction forces have a magnitude that is bounded by the normal force magnitude. This creates an awkward coupling between the constraint multipliers… Therefore, we use a simpler friction model where the friction force is bounded by a constant value." The 2006 formulation restores the coupling by clamping against — which is the version to implement.
Consequences for a pool table. PGS costs one pass per contact per iteration, is trivially incremental (stop early and you get a worse but valid answer), and warm-starts beautifully. Its known weakness is convergence along chains of contacts — the tall-stack problem — which is why shock propagation [163] [P] and mass splitting [159] [P] exist. Coumans documents the practical bound: "When only using 4 to 10 solver iterations, the projected Gauss Seidel numerical method often fails to converge to a proper solution", especially at large mass ratios [176]. On a table, contacts are almost all pairwise and short-lived, so PGS converges quickly — but a frozen rack at the break, and a ball settling into a pocket jaw, are precisely the many-simultaneous-contact case where PGS is weakest and where the compliant formulation of §5.2 was proposed [52].
Bullet's documentation makes the lineage explicit: it "uses its btSequentialImpulseConstraintSolver by default", and its acknowledgements credit "Erin Catto, Blizzard: accumulated impulse in sequential impulse" [141] [C].
A velocity-level solver cannot by itself remove existing penetration, because a velocity constraint says nothing about position. The classical fix is Baumgarte stabilisation — Baumgarte, J., "Stabilization of constraints and integrals of motion in dynamical systems", Computer Methods in Applied Mechanics and Engineering 1(1):1–16, 1972 [151] [P] — which adds a term proportional to the position error to the velocity constraint, so the error obeys a stable ODE. Catto states it as and gives a tuning recipe that is worth repeating because it is entirely empirical: set the bias factor to zero, raise it until the simulation goes unstable, then use half that [150]. In the contact solver it appears as the bias velocity
with the slop permitting a small permanent penetration so that resting contacts do not buzz [150].
Baumgarte's defect is that it injects energy: the bias velocity is a real velocity, so a body pushed out of penetration keeps the extra kinetic energy and stacks visibly bounce. Two remedies are standard.
Split impulses / pseudo-velocities. Run a second, parallel integration in a pseudo-velocity used only for position correction and then discarded, so penetration is removed without the body gaining energy. There is no paper; the citable sources are the engine itself and its developers. Bullet's btContactSolverInfo ships m_erp = 0.2 (commented "used as Baumgarte factor"), m_erp2 = 0.2 ("used in Split Impulse"), m_splitImpulse = true (on by default), and m_splitImpulsePenetrationThreshold = -0.04f — so split impulse engages only past 4 cm of penetration [141,142] [C/S]. On the project forum, Dirk Gregorius states flatly that "pseudo velocities and split impulses are the same", and Coumans that "Baumgarte sometimes adds unwanted energy/velocity due to deep penetration correction" [142] [C]. Coumans' GDC 2014 framing is the cleanest one-liner: "we may solve those levels combined (coupled) as in Baumgarte stabilization, or we may solve the velocity and position constraints as two independent problems" [176].
Soft constraints. Catto's GDC 2011 talk [150] [C] derives ODE's CFM and ERP parameters from a damped harmonic oscillator, reparameterised by damping ratio and angular frequency rather than raw stiffness and damping — his own account is that he first assumed these were "just hackery without any real mathematical underpinning… but I was wrong". The mapping is = CFM (softening) and = ERP (position feedback), with the implementation caveat that must be divided by when the engine works in impulses rather than forces. The payoff is that a soft constraint is "mathematically… not a spring at all, but rather a modified rigid constraint", hence unconditionally stable inside the solver at any stiffness, unlike an explicit spring. (The ODE manual's own formulas are and , with ERP defaulting to 0.2 [191] [C].)
A third line attacks the step size instead of the solver. Macklin et al., "Small Steps in Physics Simulation", SCA 2019 [160] [P], argue that for a fixed compute budget it is better to take many small substeps with few iterations each than few large steps with many iterations, because integrator error rather than solver convergence dominates. The related position-based line is XPBD [161] [P], which makes stiffness independent of iteration count and time step, and Müller et al.'s extended-PBD rigid bodies [162] [P]. Catto's Solver2D study [177] [C] implements and compares eight variants on the same benchmarks — PGS with Baumgarte, PGS with nonlinear Gauss–Seidel position correction, block PGS, soft PGS, and four TGS/XPBD variants — and TGS-Soft became "Soft Step" in Box2D v3.0.
A substepping trap worth naming, because it cost this project real time. If a substep loop divides the frame into steps of but executes only of them — an easy off-by-one when the accumulator is carried in floating point — the simulation runs at of real speed, silently and uniformly. It does not present as jitter. It presents as everything being slightly slow, which is very easily misdiagnosed as a friction coefficient needing a tweak.
Coulomb friction confines the contact force to a cone of half-angle about the normal, . This is a second-order cone, and it is what makes the contact problem a nonlinear complementarity problem.
The standard remedy is to facet it. Replace the circular cone by the polyhedral cone generated by for tangent directions, write the friction impulse as with and , and the constraint becomes linear — which is what makes Anitescu and Potra's solvability result possible (§5.5). The direction set must be balanced: for every there is a . Stewart's SIAM Review survey [169] [P] derives the accompanying auxiliary variable as the Lagrange multiplier of the maximum-dissipation programme subject to , giving . Dimensionally is a velocity: in sliding it equals the sliding speed along the best-opposing facet, friction saturates on the cone boundary, and complementarity typically leaves one non-zero — "the one which maximizes power dissipation"; in rolling ; in separation it "may take on any positive value… and has no meaningful physical interpretation." (Notation warning for anyone reading across this literature: Stewart, Stewart and Trinkle, and Preclik all use for the multiplier and for the tangential weights, while some graphics course notes use the opposite convention.)
Games take the cheapest version, — a pyramid — which in a 2-D tangent plane means clamping the two tangential components independently to , exactly the form in Catto's slides (§5.6).
What faceting costs is anisotropy, and it has been quantified — on rolling spheres. Le Lidec et al. [178] [R/P] state the mechanism: "the linearization of the second-order cone causes the loss of the isotropy for friction… By choosing the axes for the facets of the pyramid and due to the maximum dissipation principle, it is established that one incidentally biases the friction forces towards the corners", demonstrated with a cube given velocity along that acquires a spurious velocity and "deviates from the expected straight-line trajectory."
The directly relevant experiment is Preclik's [179] [T] (a student thesis, not peer-reviewed, but the measurement is the point): 19 spheres launched at 5° increments across a quadrant with an eight-facet cone. "Since for all spheres with initial angles not equal to 0, 45 or 90 degrees there exists no corner of the polyhedral friction cone which exactly opposes the contact velocity, the linear velocity does not decrease straightly towards zero… Particularly for the spheres rolling in the 0, 45 and 90 degree directions the relative contact velocity decreases the fastest." Only five distinct velocity profiles emerge among 19 headings — the pyramid's symmetry group is directly visible in the output. His inclined-plane test, with a facet corner pointing exactly uphill, reproduces to five decimals, so the error is purely directional and vanishes on-axis.
This is why the friction cone matters more for cue sports than for most games. §1.3 showed that the entire post-impact trajectory of a cue ball is set by a constant friction force whose direction is the slip direction — and across a game's shots that direction is uniformly distributed over the circle. A solver whose effective friction depends on the heading relative to an arbitrary tangent basis will make the same shot behave differently depending on which way the table happens to be oriented. The corroborating evidence is Bullet's documented rolling-friction bug (§5.10), which is exactly this pathology, and whose accepted fix was to use a true cone. The older standard citations for corner bias are Trinkle et al. [180] [P] and Acary and Brogliato §13.3.7 [166] [B].
Solving the true cone. The reference work is Acary, V. and Brogliato, B., Numerical Methods for Nonsmooth Dynamical Systems: Applications in Mechanics and Electronics, Lecture Notes in Applied and Computational Mechanics vol. 35, Springer, 2008 [166] [B], DOI 10.1007/978-3-540-75392-6. The nonsmooth contact dynamics (NSCD) tradition descending from Moreau and Jean does not facet at all: it is fully implicit, and solves each local one-contact problem against the true cone inside a block Gauss–Seidel sweep. Moreau's foundational chapter [186] [B] introduces the governing idea — that "the velocity is not supposed to be a differentiable function of time, but only to have locally bounded variation, so the role of the acceleration is held by a -dimensional measure", and dynamics is then governed by measure differential inclusions, "which treat possible velocity jumps on the same footing as smooth motions." The numerical papers are Moreau's sweeping process [187] [P] and Jean's NSCD method [188] [P], which sit in the same 1999 CMAME special issue as Anitescu, Potra and Stewart's three-dimensional time-stepping paper and can be cited as a unit.
Two graphics papers show what the true cone buys and how to get it:
And the true cone need not be expensive, which undercuts the usual justification for faceting. Tasora and Anitescu [185] [C/P] give a closed-form non-expansive cone projection costing a handful of flops per contact: with , , , , .
For a ball on cloth there is an even cheaper answer. Because §1.3 proves the slip direction is constant throughout a sliding phase, a 4-direction basis aligned to at the start of the phase stays aligned for the whole phase — so the pyramid's anisotropy costs nothing, provided the basis is chosen from the physics rather than from the world axes. This is, I think, a small but genuinely useful observation that falls out of putting §1.3 and §5.8 next to each other.
One distinction to keep clean, flagged by Le Lidec et al. [178]: Anitescu's convex relaxation has a different artefact from the pyramid's. It permits rather than , "which results in objects interacting at distance when contact points are sliding." Do not conflate the two errors. Two further routes are worth naming. Kaufman et al.'s staggered projections [190] [P] solve the coupled normal–friction problem by alternating projections onto two convex sets rather than decoupling them, which is the standard reference for treating the coupling properly rather than approximating it away. And Todorov's convex, analytically invertible contact model [164] [P] smooths the contact problem into a convex optimisation that can be inverted — the formulation underlying MuJoCo, and a reminder that "solve the true cone" and "keep the problem convex" are not the same goal.
And as Acary, Brémond and Huber conclude after a systematic comparison [189] [P]: "there is no universal solver."
A solver-based engine must decide when to stop simulating a body, both for performance and because an iterative solver leaves residual jitter that never quite reaches zero. The striking finding here is how little of this is published.
Bender, Erleben and Trinkle's Computer Graphics Forum state-of-the-art report [136] [P] is a 39-page survey of the entire field of interactive rigid-body simulation. The word "sleeping" appears in it twice, and "island" not at all — and both mentions are in the benchmarking section, framing sleeping as a defect to test for rather than a technique with published criteria:
"Figure 17: Test-cases that can reveal problems with freezing (sleeping policies). If too aggressive bricks will hang unnaturally in the air."
So the honest statement is stronger than "thinly covered": sleeping and deactivation are essentially absent from the peer-reviewed rigid-body literature. They live in source code, engine manuals and developer forum posts. The nearest published treatment is a book chapter — Lengyel, E., "A Jitter-Tolerant Rigid Body Sleep Condition", in Game Engine Gems 2, A K Peters / CRC Press, 2011 [137] [B] — whose text could not be obtained, so no specific method should be attributed to it.
What exists in practice, and is worth recording because a paper may need to justify its own choice:
cyclone-physics [138] [S] accumulates motion = bias·motion + (1−bias)·(v·v + ω·ω) with bias = 0.5^Δt, sleeping when motion falls below an epsilon and clamping it at 10·epsilon so that a just-struck body cannot take arbitrarily long to settle again. The 0.5^Δt makes the criterion frame-rate independent, which a naive per-frame decay is not.max(|v| + |ω|·maxExtent, 0.5·(1/Δt)·(|Δposition| + |Δrotation|·maxExtent)), with a 0.5 s time-to-sleep and a threshold of 0.05 length units per second. Two things are better here than in the older two-threshold (linear and angular) scheme: the speed is measured at the fastest point on the body, so a large slowly-rotating body is treated correctly; and solver position correction counts as motion, so a body that is being continuously depenetrated cannot fall asleep while still being pushed.5e-5 · speedScale², a wake counter giving a minimum time before sleep is possible, and the rule that a body will not sleep while touching an awake body.A failure mode directly relevant to a pool table. Simulation islands in Bullet are built from broadphase AABB overlap; Coumans notes on the project forum [142] [C] that an over-large world size makes AABBs huge and "everything will be merged into one island". Twenty-two balls plus six cushions merging into a single never-sleeping island is a realistic hazard for exactly this application.
And the observation that ties this back to §5.1. For cue sports specifically, the sleeping problem largely dissolves under an event-driven formulation, because the stopping time is analytic. From the transition times used by pooltool [44,45]:
You do not need a velocity threshold and a heuristic timer, because you can solve for the instant the ball stops. The generic engine literature has no published sleeping criteria because sleeping is a symptom of solving a general problem approximately; the billiards literature never needed them because the specific problem has a closed form. That contrast is, I think, the single most useful thing this review has to say about engine architecture for cue sports.
§1.4 established that rolling resistance on cloth is well modelled as a constant deceleration , measured at 0.0127–0.0129 for snooker [4]. Reproducing that in a general-purpose engine turns out to be surprisingly hard, and the reasons are documented.
The correct form has a length scale. In the discrete-element-method literature, where rolling resistance has a substantial peer-reviewed treatment, the rolling-resistance moment is limited by
i.e. coefficient × effective contact radius × normal force. This is the form given in Itasca's PFC documentation [149] [C], following Iwashita and Oda's elastic–plastic rolling-stiffness model [146] [P]. Ai, Chen, Rotter and Ooi [145] [P] review and classify the commonly used DEM rolling-resistance models and propose a more general one, assessing each against benchmark cases in both dynamic and pseudo-static regimes. Wensrich and Katterfeld [147] [P] make the conceptually important point that rolling friction in DEM is often a surrogate for particle shape, not a surface property. Kuhn [148] [P] distinguishes two mechanisms: a contact moment between particles, versus a reduction of the tangential contact force with no moment at all.
Both mainstream game engines omit the radius. Bullet's torsional/rolling friction constraint limits the torque to μ_r · P_n with no radius term, so a 26 mm ball and a 1 m ball behave identically at the same coefficient. Box2D v3.1's rollingResistance is structurally the same — a warm-started angular impulse driving relative to zero, clamped to — and also has no radius term. PhysX [140] exposes no rolling friction at all, which is the origin of the perennial "my ball never stops" complaint in the Unity and Unreal ecosystems.
Bullet's own demo advises against using it. The RollingFrictionDemo source comments state that spheres come to rest on a slope using the constraint where damping cannot, but "generally it is best to leave the rolling friction coefficient zero (or close to zero)" [143] [S].
Three documented defects, all of which a cue-sports engine would hit:
btSequentialImpulseConstraintSolver, both the spinning and rolling rows are nested inside a test on the rolling friction coefficient, so setting spinning friction alone applies nothing. Bullet issue #4188 (opened 2022) remains open [144] [S]. For a cue-sports engine this is precisely the bug that would kill vertical-axis spin decay — the §1.5 effect — while leaving everything else looking correct.A correction worth recording. It is sometimes said that rolling-resistance implementations can reverse a body's spin or inject energy. I asked for sources on this and none were found; the code contradicts it. Both Bullet and Box2D accumulate a clamped impulse targeting , and because the clamp is applied to the accumulated impulse the constraint can at worst bring to zero, never past it. The hazard is real for a naive torque formulation — Guendelman-style schemes guard against it explicitly by preserving sign — but the constraint formulations avoid it by construction. I record this because it is a plausible-sounding claim that turns out not to hold.
Erin Catto and Dirk Gregorius discussed the design on the Bullet forum [142] [C]: Catto proposes a velocity constraint with the torque limited by coefficient × a length scale × sum of normal forces — explicitly including the length scale both shipped engines omit — and notes that "most people simply use angular damping", while conceding that damping cannot reproduce static rolling friction, nor handle a ball at rest on a platform moving at constant speed.
Drawing §§5.1–5.10 together, the three families answer different questions, and a VR cue engine has requirements that cut across them.
| Event-driven analytic | Impulse solver (SI/PGS) | Compliant (penalty) | |
|---|---|---|---|
| Accuracy within a phase | Exact to the model | Integration + solver error | Integration error only, but stiff |
| Cost | ∝ number of events | ∝ contacts × iterations × steps | ∝ steps, and the steps must be tiny |
| Many simultaneous contacts | Poor — event storms | Adequate; degrades along chains | Best — no arbitration needed |
| Determinism | Excellent | Good | Poor with adaptive integrators |
| Arbitrary geometry, a tracked cue, balls off the table | Impractical | Routine | Routine |
| Real-time at 90 Hz | Yes, easily | Yes | No, for phenolic stiffnesses |
| Physical completeness of the contact | Assumed | Assumed | Resolved |
The tension for VR is specific and, as far as I can tell, unaddressed in the literature. The ball–ball, ball–cloth and ball–cushion parts of the problem are exactly what the event-driven method was built for, and it is both faster and more accurate there than any solver. But a VR engine must also simulate a rigid body under external control at arbitrary velocity — the cue — plus the possibility that the player reaches in and disturbs the table, and that balls leave it. Those are general rigid-body problems with no analytic form.
Two hybrid architectures suggest themselves, neither of which appears to have been published:
Two design constraints follow from the review and are worth stating as such:
This section is deliberately short, but every claim in it is load-bearing for a cue-sports engine, because a cue stroke is close to the worst case for a consumer VR tracking system: a fast, self-paced, ballistic motion of a held implement, ending in an impact, whose outcome is determined almost entirely by the state of the implement in the few milliseconds around that impact.
The widely quoted "20 ms" target is not a psychophysical result. It comes from Carmack's practitioner article "Latency Mitigation Strategies" (2013) [53] [C], which states that below approximately 20 ms delays "are generally imperceptible" and that 50 ms "will feel responsive, but still noticeably laggy". The same article introduces the mitigation techniques — late input sampling, view bypass, and time warping the rendered frame against updated pose — that are the direct ancestors of asynchronous timewarp, and which are why a device's effective latency is lower than its pipeline latency. Stauffert, Niebling and Latoschik's peer-reviewed review [54] [P] is the right citation for this provenance, because it performs the attribution explicitly rather than repeating 20 ms as folklore.
The psychophysical numbers are different and, for the most part, smaller:
Two cautions. First, absolute detection thresholds in slow manual pointing are far higher than change JNDs during fast motion — Yang et al. [60] [P] report a latency perception threshold of 130–170 ms in a Fitts'-law pointing paradigm, while also showing (via EEG) that latency was registered neurally even when participants were unaware of it. Conflating the two families of number is a common error. Second, Stauffert et al. [54] emphasise that latency jitter — spikes and periodic patterns — provokes cybersickness more reliably than a higher but steady latency does, and that prediction reduces jitter at the cost of over-anticipation artefacts.
The single most relevant paper for a VR cue engine is Warburton, Mon-Williams, Mushtaq and Morehead, "Measuring motion-to-photon latency for sensorimotor experiments with virtual reality systems", Behavior Research Methods 55(7):3658–3678, 2023 [61] [P], DOI 10.3758/s13428-022-01983-5. Their point is that previous methods report a single averaged latency and thereby hide the effect of the motion-prediction algorithms in modern runtimes. Using a high-speed-camera technique that co-registers real and virtual controller motion through a movement, on HTC Vive, Oculus Rift, Rift S and Valve Index under Unity + SteamVR, they found:
at the start of a sudden movement, mean latencies of 21–42 ms; once prediction converges, latency falls functionally to 2–13 ms; convergence takes ~25–58 ms from movement onset.
Their own stated implication is that "sudden accelerations (e.g., movement onset, impacts, and direction changes) will increase latencies and lower spatial accuracy." A cue strike is a sudden acceleration terminating in an impact. The shot is decided during the system's worst tracking window.
Two supporting results sharpen this:
The engineering conclusion, which a VR cue-sports engine should probably state as a design principle: derive the strike from tracked velocity at the moment of contact, not from acceleration, and do not trust the pose stream in the frames immediately around movement onset.
On tracking rates: Niehorster, Li and Lappe [65] [P] give the cleanest peer-reviewed account of the architecture — IMU dead reckoning at high rate underneath, lighthouse correction at 120 Hz, application-visible pose consumed at the 90 Hz display refresh — together with an end-to-end latency of about 22 ms measured with a 480 Hz camera, sub-0.03 mm² positional jitter, and the important failure mode that regaining tracking after a loss produces large position and orientation offsets that no one-time calibration can fix. Bauer, Lienhart and Jost [66] [P] independently found systematic errors of several centimetres including a ~0.4° reference-plane tilt on a Vive Pro at a geodetic calibration facility, and Sansone et al. [67] [P] replicated the tilt and found 0.5 ± 0.2 cm accuracy in plane but 1.7 ± 1.2 cm on the depth axis. Kuhlmann de Canaviri et al. [68] [P] confirmed against a UR10 robot that dynamic accuracy "suffers from increasing velocity" and that more base stations help.
Vendor controller sample rates are not published, in any peer-reviewed source or spec sheet I could locate. Any device-specific pose rate a paper relies on is that paper's own measurement and should be presented as such.
Two older results are worth citing because they establish which lag matters. Ware and Balakrishnan [69] [P] found that lag in head tracking is relatively unimportant for performance, whereas lag in hand tracking is critical, and modelled hand lag as a multiplicative term on Fitts' index of difficulty — so small lags cause large degradation when targets are small, which is the cue-sports case exactly. They also measured higher error along the in/out-of-screen axis, foreshadowing the depth-axis finding above. MacKenzie and Ware [70] [P] quantified it: at 225 ms lag, movement times rose 64% and error rates 214%, and the multiplicative model explained 94% of variance.
Kelkkanen et al. [71] [P] studied hand-controller latency and aiming accuracy directly in 6-DoF VR, finding accuracy degrading beyond ~130 ms latency for predictable target motion and ~90 ms for unpredictable motion — a cue shot is a self-paced aim at a static target, i.e. the forgiving end of that space, which is some comfort.
The canonical reference is Kuchenbecker, Fiene and Niemeyer, "Improving contact realism through event-based haptic feedback", IEEE TVCG 12(2):219–230, 2006 [72] [P]. Their observation — "tapping on surfaces in a typical virtual environment feels like contact with soft foam rather than a hard object" — and their fix, superimposing high-frequency transients scaled by impact velocity on the position-based force, is directly the model a cue-tip-on-ball event wants. In their study, event-based feedback made virtual samples rate as woodlike where position feedback alone rated as foam.
Okamura, Cutkosky and Dennerlein [73] [P] measured stylus acceleration during real tapping, modelled it as a decaying sinusoid, and then found — crucially for implementation — that the most realistic parameters differ from those fitted to the measured acceleration, because display bandwidth limits faithful replay. Tune by perception, not by fitting physics. Choi and Kuchenbecker [74] [P] survey the actuator technology and are the right citation for the hardware ceiling on rendering a strike transient through the LRA in a consumer controller. Lopes, Ion and Baudisch's Impacto [75] [P] shows what full impact rendering would take: decomposing the sensation into a solenoid tap (tactile) plus electrical muscle stimulation (the impulse), rated more realistic than either alone, and explicitly designed to be merged into props such as a bat.
On the weight and inertia of a held virtual implement, the pseudo-haptic literature (Lécuyer [76] [P]; Samad et al. [77] [P]; Ujitoko and Ban [78] [P]) manipulates the control–display ratio to induce perceived weight. For a cue this must be flagged as a tension rather than a solution: distorting hand-position gain is precisely what one must not do during an aimed stroke, and Ujitoko and Ban single out "the visible mismatch between visual stimulus and user input" as an unsolved design problem — which a precision aiming task will expose immediately.
Kilteni, Groten and Slater [79] [P] give the standard decomposition: sense of embodiment = self-location + agency + ownership. Waltemate et al. [80] [P] provide the key latency dissociation — in a virtual-mirror task with delays from 45 to 350 ms, motor performance and simultaneity perception degrade above 75 ms, while agency and ownership hold until about 125 ms and do not collapse even at 350 ms. The hand stops working before it stops feeling like yours.
Argelaguet et al. [81] [P] report a counter-intuitive result with direct design consequences: agency is stronger for less visually realistic hand representations, because they produce less mismatch between action and animation, while ownership is stronger for the realistic hand. Applied to a cue, this suggests a schematic, perfectly-tracked implement may outperform a beautifully rendered one that lags.
Weser and Proffitt [82] [P] review tool embodiment and argue that expertise moderates it: experts embody tools differently from novices, and may show enhanced body-representation change when using their own personal tool. For a cue-sports study this makes player skill a variable that must be measured, not controlled away — and it implies a real player's own cue is not interchangeable with a generic virtual one.
The ball-sports VR training literature is small but consistently positive; Richlan et al. [83] [P] review twelve pre/post intervention studies (aggregate N = 493) and Neumann et al. [84] [P] give the earlier systematic review. Miles et al. [85] [P] supply the framing distinction between perceptual fidelity (how the scene looks) and functional fidelity (whether the task behaves right), arguing the latter matters more for sports training — which is an argument for spending engineering effort on physics rather than rendering.
The strongest transfer results are Gray's baseball study [86] [P] — adaptive VE training beat extra real batting practice on an on-field test, and predicted league statistics and competitive level over the following five years — and Michalski et al.'s table tennis study [87] [P] (N = 57, blind expert coach ratings, for real-world improvement, though against an untrained control). Oagaz, Schoun and Choi [88] [P] provide the necessary nuance: VR table tennis training significantly improved technique but produced no significant change in balls returned at real-world retention. Wu et al.'s SPinPong [89] [P] is the closest analogue to a cue engine — it trains the return of a fast spin shot using a haptic racket, visual cues and temporal distortion.
Two findings deserve special attention because they are cautionary. Harris et al. [90] [P] found that novice golfers trained in VR improved real putting comparably to real practice — but in Experiment 2, skilled golfers who used VR immediately before real putting showed impaired quiet-eye gaze control and worse putting accuracy. VR as a harmful warm-up for experts is a result any cue-sports engine paper should engage with, since a substantial fraction of its users will be real players. Their companion paper [91] [P] sets out the fidelity/validity framework for a putting simulator.
My first pass through this literature concluded that no peer-reviewed HMD cue-sports study existed at all. A second, independent search showed that to be wrong, and the correction is worth recording because the distinction it turns on is exactly the one a VR cue engine has to reason about.
Work that exists. The strongest reference is Haar, S., Sundar, G. and Faisal, A. A., "Embodied virtual reality for the study of real-world motor learning", PLOS ONE 16(1):e0245717, 2021 [95] [P]. Participants hold a real cue and strike a real ball on a real table, while an HTC Vive Pro renders a veridically aligned virtual scene, with OptiTrack streaming the cue pose at 90 Hz. Two things make it directly load-bearing here. First, the rig was validated by comparing real-world ball trajectories against the physics engine's computed virtual trajectories — that is precisely the validation methodology a VR cue engine paper should adopt. Second, the finding is a measured fidelity deficit: learning in embodied VR resembled the real task but was slower and weaker, and intertrial variability showed no short-term learning trend in VR, unlike the real world. Their non-VR baseline is Haar, van Assel and Faisal [96] [P], which instrumented first-time billiards learners with full-body wearables and found motor learning affects the whole body.
Haptics for cue sports has been attempted: Gourishankar, Srimathveeravalli and Kesavadas, "HapStick: A High Fidelity Haptic Simulation for Billiards", World Haptics 2007 [97] [P]; De Paolis, Aloisio and Pulimeno [98] [P], whose XVR implementation is a useful contrast because it drives the balls with a generic rigid-body engine (ODE) rather than a billiards-specific analytic model; and Thai and Kumar [99] [P], a 2×4 mixed-factorial transfer-of-training study (N = 38) on how haptic feedback in a mixed-reality pool game affects real-life pool performance. De Grandpré et al. [100] [P] recorded EEG during virtual pool shots with 30 participants, explicitly motivated by "the lack of realistic sensory feedback from traditional VR controllers".
The augmented-reality line assists play on a physical table: Uchiyama and Saito [92] [P] overlay predicted ball behaviour from a single handheld camera view with no markers or magnetic sensors; Sousa, Alves and Rodrigues [93] [P] project trajectories, effects and menus onto the cloth from a projector-plus-Kinect rig, reporting 98% shot-prediction accuracy when no cushion bounce occurs — a qualifier that says a great deal about the state of cushion modelling (§3.6); Yan, Zhan and Yang [101] [P] present a head-mounted AR billiards learning assistant. Wu and Dellinger [94] [P] describe capturing reality for a billiards simulation with a tangible physical mini-table.
What is still missing. What none of this covers is the case a VR cue-sports engine actually occupies: a fully virtual table, cue and balls, struck with a tracked controller, with the physics entirely simulated. Every study above either keeps the physical equipment (Haar et al.; the real-cue attachments in §7), augments a real table (Uchiyama; Sousa; Yan), or evaluates a haptic device rather than the simulation. Richlan et al.'s review of VR sports training [83] lists archery, bowling, curling, darts, golf, baseball, table tennis, goal sports and martial arts — no cue sport appears at all.
Nor could either search pass find any peer-reviewed evaluation of the physics accuracy of a commercial cue-sports product against real-table measurements (§7.3). So the defensible gap statement is narrower than "nobody has studied VR billiards", but it is still a real one: the perceptual, embodiment and fidelity questions specific to striking a virtual ball with a virtual cue have not been asked, and no published work measures how closely any VR or commercial cue-sports simulation reproduces measured real-table behaviour.
The academic simulation work forms a single connected tradition, and it is worth laying out because almost every later system inherits from it.
Queen's University (Greenspan's group) produced the event-based simulator (§5.1) [40,41,42] and the Deep Green robot: a ceiling-mounted industrial gantry over a full-size pool table, integrated with the same physics simulator used in the computational pool tournaments, and described by its authors as playing "at an above-amateur level" [102] [P]. The robotics papers are Long et al. [103] [P] on automatic potting, Greenspan et al. [104] [P] on whether computational intelligence is needed at all, Lam and Greenspan [105] [P] on eye-in-hand visual servoing, and a US patent on dual-camera positional error correction [106] [C] (a global overhead camera locates the balls; a cue-aligned local camera corrects accumulated positioning error). Leckie and Greenspan also applied Monte-Carlo tree search to pool strategy [107] [P].
Stanford (Shoham's group) produced CueCard, the 2008 tournament winner, and the clearest framing papers: Archibald, Altman and Shoham, "Analysis of a Winning Computational Billiards Player", IJCAI-09 [108] [P], and Archibald, Altman, Greenspan and Shoham, "Computational Pool: A New Challenge for Game Theory Pragmatics", AI Magazine 31(4):33–41, 2010 [109] [P]. A distributed follow-up appeared in IEEE TCIAIG in 2016 [110] [P].
Alberta produced PickPocket — Smith, M., "PickPocket: A computer billiards shark", Artificial Intelligence 171(16–17):1069–1091, 2007 [111] [P], and the earlier AAAI paper [112] [P]. PickPocket won the simulated 8-ball tournaments at the 10th and 11th Computer Olympiads by adapting classical game-tree search to a continuous, stochastic domain.
Other approaches: Landry and Dussault's optimisation-based player [113] [P]; Chua, Wong and Koo's zero-order Sugeno fuzzy decision system [114] [P] (note the title often mis-cited as "Decision algorithm for pool using fuzzy system"); reinforcement learning at macro and micro scales [115] [P]; backward induction plus machine learning for a "human-like" bot [116] [P]; and Fragkiadaki et al.'s visual predictive models, which used billiards as the test domain for learning physics from pixels [117] [R/P].
The most recent and most relevant to a modern engine is CueTip — Memery, S. et al., "CueTip: An Interactive and Explainable Physics-aware Pool Assistant", SIGGRAPH '25 Conference Papers, 2025 [118] [P]. It builds its environment on top of pooltool [44], instrumenting the simulator to emit natural-language event traces alongside state traces for an LLM to interpret, with a trained MLP surrogate to keep interaction fast. It is a clear demonstration that the event-based lineage remains the substrate researchers reach for.
Also worth knowing: a robotic snooker player at Bristol dating to the late 1980s (the "Snooker Machine"), which led to Sang's 1994 PhD thesis [119] [T]; Ho, Martin and Baldwin's twenty-years-on retrospective [120] [P]; Nierhoff, Heunisch and Hirche on strategic play [121] [P]; and the computer-vision line from Denman, Rea and Kokaram's snooker broadcast analysis [122] [P] to Rodriguez-Lozano et al.'s 3-D reconstruction and multi-object tracking [123] [P].
The competitive series is directly useful for a VR engine because it settled a shot representation and a noise model, both of which map onto the VR input problem.
The game was 8-ball under Billiards Congress of America rules [109]. The architecture was client-server: the server holds the virtual table state and executes submitted shots on the PoolFiz simulator, with a 10-minute per-game budget per agent. FastFiz [43] is the later re-implementation; note a discrepancy in the sources — the JOSS paper describes FastFiz as "custom built for hosting the AAAI Computational Pool Tournament from 2005–2008" [44], while the AI Magazine article and FastFiz's own archive README indicate PoolFiz ran the 2005/2006/2008 events with FastFiz succeeding it ahead of 2011. I flag this rather than picking a side.
The shot is five real numbers [109]:
| Symbol | Meaning | Units |
|---|---|---|
| cue velocity | m/s | |
| cue orientation (azimuth) | degrees | |
| cue elevation above the table | degrees | |
| horizontal offset of the tip impact point from ball centre | mm | |
| vertical offset of the tip impact point from ball centre | mm |
Because the simulator is deterministic, human fallibility was modelled by adding zero-mean Gaussian noise on the server side, with standard deviations of 0.1 for , 0.125 for , 0.075 for , and 0.5 for and [109]. This is a genuinely useful reference point for a VR engine: it is a published, competition-validated estimate of how much a shot's five parameters have to be perturbed to represent a less-than-perfect player — and it is directly comparable to the tracking-error budget of §6.2. A VR system whose tracking noise exceeds the tournament's skill noise is, in effect, playing for the user.
One further detail from the IJCAI-09 paper: CueCard used its own re-implemented physics engine giving an average six-fold speedup per simulation, validated by checking that a shot simulated on both engines landed sufficiently close [108]. The theme recurs — every serious system ends up rewriting the physics for speed and then has to answer the validation question.
Everything in this subsection that touches physics quality is vendor marketing. Neither research pass found any technical documentation, whitepaper, patent, or published model for any commercial cue-sports physics engine. The claims are listed because a related-work section should record what the field asserts about itself, not because they are evidence.
Non-VR simulators.
VR titles. Corrections to several titles in common circulation: there is no "Sports Bar VR 2" (only a version 2.0 update in December 2017); the ForeVR title is ForeVR Pool, not "ForeVR Billiards"; and no "Snooker VR" or "Snooker 22" product exists.
| Title | Developer / publisher | Released | Physics claim (vendor) |
|---|---|---|---|
| Sports Bar VR (formerly Pool Nation VR) | Cherry Pop Games + Perilous Orbit | Jun 2016 (Steam), Oct 2016 (PSVR) | "an immersive and accurate representation of the sport… real pool physics"; "world renowned Physics engine… the balls will behave as they would in the real world" |
| ForeVR Pool | ForeVR Games | May 2023 (Steam), Dec 2023 (PSVR2) | "ForeVR's custom Pool physics engine, tailored for all play styles—spins, jumps, and trick shots included" |
| MiRacle Pool | Pixel Works | Jun 2024 (Quest) | "VR's #1 Pool Simulator"; "the most realistic ball physics"; "physics-first"; physics "developed over three years"; Arcade vs Simulation modes |
| Backspin: Snooker & Pool | Portus Labs | Jan 2026 (Quest) | "spin-true cue ball physics, realistic throw, and natural table roll"; tables "tuned to real dimensions and authentic pocket geometry"; Real Cue Mode to play with a physical cue |
| Unlimited Snooker | SP Software Productions | Jun 2025 (Quest) | "Novel table alignment system"; "Real Cue Attachment. Play with your own cue exactly like you do in real life" |
| Cueist / Cueist 2 | (developer unconfirmed) | — | "not a casual point and click type game, but a deeper simulation which will reward real skill"; supports "spin, swerve, massé, and jump shots" |
| Snooker Billiard | DotStudio | Jul 2026 (SteamVR) | "a custom-designed dedicated physics engine"; ray tracing in VR |
| Hustle Kings VR | XDev Studio Europe | 2016 (PSVR) | makes no physics claim |
Two patterns in that table are worth noting for a paper. First, every product that competes on fidelity claims a bespoke physics engine — none claims to use a general-purpose rigid-body engine, which is at least consistent with the argument of §5. Second, the newest titles converge on attaching a real physical cue to a tracked controller (Backspin's "Real Cue Mode", Unlimited Snooker's "Real Cue Attachment"). That is an implicit admission that controller-held virtual cues have a fidelity problem, and it aligns with Weser and Proffitt's finding [82] that experts embody their own tools.
| Project | Language / licence | Physics | Notes |
|---|---|---|---|
| pooltool [44] | Python, Apache-2.0 | Event-based (Leckie–Greenspan); Han cushion model; Numba JIT; Panda3D 3-D UI | The only one with a peer-reviewed paper; also peer-reviewed by pyOpenSci. Extensively documented [10,45] |
| FastFiz [43] | C++ with Python bindings | Successor to PoolFiz; FastFiz.h (table physics, rules-independent) + Rules.h (8-ball etc.) |
A rescue archive: the original code vanished from Stanford's billiards site and was recovered from the Wayback Machine. Original authorship and licence are not stated |
| Billiards (nongnu) [129] | Lua 5.1 on the Techne framework, GPL | "aims for physical accuracy and simplicity", "totally configurable" | Candid about limits: no jump shots, and the author states he is "not a particularly experienced billiards player" and invites feedback on the model. No Windows support |
| Foobillard++ [130] | C, GPL | Not documented | Enhanced fork of Berger's foobillard; Linux + Windows |
| PoolPhysics [47] | Python | Event-based | Not independently assessed |
No peer-reviewed evaluation of any commercial cue-sports product exists — not of its physics accuracy against real-table data, and not as an experimental apparatus in a user study. The nearest citable statements are the JOSS paper's peer-reviewed concession that "realistic physics [is] seen in some commercially-produced games (i.e., Shooterspool and VirtualPool4)" but that they "have little, if any, utility in research contexts due to closed source code and a lack of open APIs" [44]; and Alciatore's expert-practitioner assessment that "modern pool simulators like Virtual Pool do surprisingly well at matching the physics and play of a real pool table; although, they are not perfect" [131] [C] — offered without measurement data.
No differentiable billiards simulator has been published. Both research passes searched for one and found only general differentiable-physics and RL work with no billiards application. Given how well-suited the domain is — a low-dimensional, five-parameter action space with an analytic forward model — this is a conspicuous absence rather than a saturated area.
These can be stated in a paper without hedging, and an engine that disagrees with them is wrong.
Ordered roughly by how much a VR cue engine would benefit from an answer.
Listed so they are not accidentally propagated.
Source types: [P] peer-reviewed paper · [B] book · [T] thesis · [R] preprint/technical report · [C] practitioner or standards resource · [S] software · [V] vendor claim.
Numbering follows first citation within each of four research passes (classical physics and cushions; simulators, games and competitions; VR, tracking and haptics; rigid-body contact solvers), so it is grouped by topic and is not strictly monotonic through the document. Reference 51 was withdrawn during drafting and the number is unused.
(Reference 51 was withdrawn: see §5.2.)
src/body.cpp; note the header comments give a default sleep epsilon of 0.1 while src/core.cpp sets 0.3.src/solver.c and include/box2d/types.h. [S] https://box2d.orgPxRigidDynamic.h. [S] https://github.com/NVIDIA-Omniverse/PhysXbtSequentialImpulseConstraintSolver by default", and credits "Erin Catto, Blizzard: accumulated impulse in sequential impulse". Contains no documentation of sleeping or deactivation.viewtopic.php?t=3795); Catto and Gregorius on rolling-resistance design (viewtopic.php?t=1896); and an unresolved 2014 report of spheres that never stop (viewtopic.php?f=9&t=10150). Developer forum posts, not publications.examples/RollingFrictionDemo/RollingFrictionDemo.cpp. [S] — "Generally it is best to leave the rolling friction coefficient zero (or close to zero)."notesd2.pdf gives the clearest statement of the colliding-versus-resting contact split.The review above is deliberately wider than the implementation below. This is the join: everything in the literature that the engine has not taken up, and why.
A survey that only covered what was implemented would flatter the engine and teach a reader less. The list below is the honest difference, and it is where the next work is.
| From the literature | Status here | Why, and what it would cost |
|---|---|---|
| Compliant / deformable cushion. Penalty contact with a Hertz normal force and Hunt–Crossley damping, as used for pool by Gismeros Moreno et al. (2021). | Not used. | The cushion here is rigid, with the impact integrated over its own accumulated normal impulse. That is the Mathavan–Jackson–Parkin scheme, and it is better than a reflection — but it assumes the rubber does not deform, and those authors give 2.5 m/s of normal velocity as the limit of that assumption. Every hard shot in this game exceeds it, and the engine's own source labels its behaviour above that speed as extrapolation. A lumped spring-damper integrated over the real contact duration is affordable — contacts are rare and involve one ball — but the stiffness and damping constants for a billiards cushion have never been published, so it would need fitting against the same high-speed measurements. |
| Finite-element or hyperelastic modelling of the cushion profile. | Does not exist to use. | Searched for specifically. Table-tennis rubber, golf balls and tennis balls all have such studies; a billiards cushion profile appears to have none. This is a gap in the field rather than a gap in the engine. |
| Event-driven simulation. Solving in closed form for the next event rather than stepping time. | Not used. | A deliberate choice, not an oversight. This engine steps at a fixed 2 kHz because it must be bit-identical on two headsets playing lockstep, and because it carries contacts an event formulation does not close over — a ball on a curved cloth lip, a ball in a net, skittles. The event approach is more accurate per collision and less robust when many contacts coincide. |
| Sleeping criteria from the rigid-body literature. | Used, but the literature has little to offer. | The review's finding is worth repeating: sleeping is almost absent from the peer-reviewed work, and where it appears it is treated as a defect to test for. For cue sports the problem dissolves — the rolling-to-stationary time is v₀ / (μr g) in closed form. The sleep rule here exists for the balls in the troughs under the table, which are solved as general rigid bodies and therefore inherit the general problem. |
| Differentiable billiards. Gradients through the simulation for learning or control. | Not used. | Nothing in the game asks for gradients; the opponent searches by sampling simulated shots. Noted because it is where the academic interest currently is. |
| Published validation of pocket capture and rattling. | Nothing to use. | The review finds this thin. The pocket model here — three distinct circles, a cloth cut as a signed distance field, and a lip a ball rides down — is the engine's own, tuned against play rather than against measurements, because there are no measurements to tune against. |
| Ball–ball friction coefficients. | Used, with the spread the literature reports. | Throw is emergent here rather than fitted, which is the right shape; but the coefficient it depends on is one of the least well-pinned numbers in the field. |
The models, the mathematics and the algorithms, as implemented.
A reference for the "system description" chapters of a paper. Everything below is read off the source; file:line citations are given for every non-trivial claim, and code comments are quoted where they record a measurement, an experiment or a bug that motivated a choice. Where the source does not settle a question this document says so rather than guessing.
Source files described:
| File | Role |
|---|---|
mote/games/thumbycue/src/cue_physics.h |
data model (CueWorld, CueBall, CueSeg) |
mote/games/thumbycue/src/cue_physics.c |
ball motion, ball–ball, cushions, jaws, pockets, the drop |
mote/games/thumbycue/src/cue_table.c |
table geometry: beds, cushion chains, jaw curves, pocket cuts |
mote/games/thumbycue/src/cue_trig.h |
deterministic sin/cos |
mote/engine/physics/mote_phys.c |
general rigid-body solver (skittles, everything under the cloth) |
CueVR/src/cuevr_cue.c |
VR cue construction, pose ring, contact detection, stroke measurement |
CueVR/src/cuevr_hit.c |
segment–sphere contact and strike geometry |
CueVR/src/cuevr_app.c |
strike → physics hand-off, tip/ball momentum transfer, jump launch |
mote/platform/xr/mote_xr.c, mote_xr.h |
500 Hz pose sampling thread, XrSpaceVelocity |
mote/games/thumbycue/src/cue_ai.c |
opponent shot search (appendix) |
| Symbol | Code | Meaning | Value / source |
|---|---|---|---|
CueBall.pos |
ball centre, world metres; table plane is world –, up, cloth at | cue_physics.h:68 |
|
CueBall.vel |
linear velocity (m/s) | cue_physics.h:69 |
|
CueBall.w |
angular velocity (rad/s), world frame, full 3-D | cue_physics.h:70 |
|
CueBall.orient |
render orientation, a matrix integrated from | cue_physics.h:71 |
|
CueWorld.R, CueBall.r |
ball radius; per-ball r>0 overrides (English pool: 47.6 mm cue ball vs 50.8 mm objects) |
cue_physics.h:110, :623 |
|
CueWorld.mass, CueBall.m |
ball mass; per-ball override as above | cue_physics.h:111, :626 |
|
| — | , uniform solid sphere | cue_physics.c:499, :444, :661 |
|
CueWorld.g |
9.806 m/s² | cue_physics.c:47 |
|
rc |
centre→cloth-contact vector, | cue_physics.c:488 |
|
u |
contact-point slip, , horizontal part only | cue_physics.c:490–492 |
|
mu_s |
ball–cloth kinetic (sliding) friction | 0.20 default, cue_physics.c:53 |
|
mu_r |
rolling resistance | 0.010 default, cue_physics.c:54; per-table override cue_table.c:2899 |
|
spin_decel |
vertical-spin angular deceleration, with | cue_physics.c:57 |
|
CUE_H, g_sub_h |
integrator substep, s | cue_physics.c:18, :2568 |
|
V_STOP |
0.005 m/s linear rest threshold | cue_physics.c:33 |
|
W_STOP |
0.05 rad/s spin rest threshold | cue_physics.c:35 |
| Symbol | Code | Meaning | Value / source |
|---|---|---|---|
e_bb |
ball–ball restitution | 0.96, cue_physics.c:58 |
|
mu_bb |
ball–ball friction (throw) | 0.06, cue_physics.c:59 |
|
e_cush |
cushion restitution at zero approach speed | per table, 0.930–0.990, cue_table.c:76–108 |
|
cush_efall |
fall of per m/s of approach | 0.038–0.062 per table, same | |
e_cush_min |
floor on cushion restitution | 0.55, cue_table.c:108 ("Marlow's rails, as the floor") |
|
mu_cush |
ball–cushion friction | 0.12, cue_physics.c:63 |
|
cush_spin |
fraction of the rail friction impulse that becomes new vertical spin | 0.30, cue_physics.c:64 |
|
cush_sin, cush_cos |
, of the contact-normal tilt; stored as sine/cosine, never as an angle | cue_physics.h:196–203, cue_table.c:2843–2846 |
|
e_bed |
slate/cloth restitution for a landing ball | 0.40, cue_physics.c:75 |
|
v_land |
below this downward speed a landing settles flat (~1 mm of rise) | 0.14 m/s, cue_physics.c:76 |
|
CUE_CUSH_STEPS |
impulse increments per cushion impact | 96, cue_physics.c:731 |
|
CUE_CUSH_RIGID_V |
where the rigid-cushion assumption stops holding | 2.5 m/s, cue_physics.c:735 |
| Symbol | Code | Meaning | Source |
|---|---|---|---|
pocket[p] |
pocket point | cue_table.c:1481 |
|
pmnorm[p] |
outward mouth normal, from the two nearest jaw tips | cue_table.c:3176–3181 |
|
drop_c[p] |
drop / catch / bore circle centre | cue_table.c:3184–3188 |
|
pocket_r[p] |
drop-circle radius visible mouth cap_* |
cue_table.c:2921 |
|
cut_c[p] |
cut (cloth-edge) arc centre | cue_table.c:4368 |
|
cut_r[p] |
cut arc radius cut_ref cut_rad |
cue_table.c:4369 |
|
lip_d[p] |
lip roll radius: how far the cloth turns over the cut edge | cue_table.c:4370, capped :4398–4404 |
|
rr |
ball-centre arc radius on the lip, | cue_physics.c:1155, :2080 |
|
cue_phys_cut_out |
signed distance past the cloth edge: on cloth, at the edge, over the drop | cue_physics.c:1086 |
| Symbol | Code | Meaning | Source |
|---|---|---|---|
CueVrCue.axis |
cue axis, butt→tip, unit | cuevr_cue.c:1704, :2128 |
|
CueVrCue.tip |
tip position, room space | cuevr_cue.c:1705 |
|
s_tip_r |
cue-tip radius | cuevr_cue.c:175–180 |
|
reach |
contact radius | cuevr_cue.c:2628 |
|
tip_side, tip_vert |
tip contact offset as a fraction of ; right english, top | cuevr_cue.c:2866–2867, cuevr_hit.c:41–48 |
|
elev |
cue elevation above horizontal (butt raised) | cuevr_cue.c:2573, cuevr_hit.c:49–54 |
|
CUE_TIP_MAX |
miscue limit, 0.50 of the radius | cue_physics.h:878 |
|
CUE_SQUIRT_RAD |
cue-ball deflection (squirt) at full side, 0.0349 rad = 2.0° | cue_physics.h:688 |
The world is planar by construction but not by assumption. The header states the
convention: "Table plane = world X–Z, Y up; cloth at Y = 0, ball centre at Y = R
(v1 is planar — no jumps, vel.y and pos.y are pinned). Each ball carries full 3-D
angular velocity w so spin physics is real: the horizontal components are
top/back/roll, the vertical (Y) component is side / 'english'"
(cue_physics.h:5–8). Vertical motion was subsequently enabled for jump shots,
cushion hops and the drop; the cloth-contact path still flattens a falling ball
(cue_physics.c:545) while leaving a rising one alone — a fix whose reason is
recorded verbatim: "This flattened it unconditionally … and is also why a ball could
never leave the cushion: the impact handed it an upward velocity and the next
substep of cloth contact threw it away before the airborne test upstairs ever saw
it" (cue_physics.c:538–545).
Orientation is integrated separately as a rotation matrix, by rotating about
through each substep
(ball_spin_orient, cue_physics.c:549–555). It is render-only: nothing in the
dynamics reads orient.
Each substep, for each ball on the cloth, ball_cloth() (cue_physics.c:486–546)
computes the slip of the ball's contact point on the cloth,
and in the horizontal plane. The regime is decided by comparing against the maximum slip one substep of full kinetic friction can remove:
The is the combined linear + angular decay rate of the contact-point slip
for a uniform sphere: with
. The code states this: "The contact-point slip |u| decays under
kinetic friction at (7/2)·μ_s·g (the combined linear + angular effect for a uniform
sphere)" (cue_physics.c:494–497).
(a) Sliding, (cue_physics.c:501–507):
This is what develops draw/follow/stun into their eventual roll; the header calls it
out as such (cue_physics.h:14–16).
(b) The transition substep,
(cue_physics.c:511–518): the same impulse is applied but scaled by
, i.e. exactly enough friction to zero the residual
slip — "energy-exact, no snap bump" (cue_physics.c:509–510).
(c) Rolling (cue_physics.c:521–530): with ,
The angular velocity is slaved to the rolling constraint rather than integrated, so
the slip stays identically zero. A sign check zeroes if the resistance
term overshoots (cue_physics.c:525).
Note that U_ROLL (cue_physics.c:34, "0.01 m/s contact-point slip => rolling") is
defined but never referenced anywhere in the source; the rolling transition is
decided by the comparison above.
decays linearly and independently of the ball's motion, with a deadband
(cue_physics.c:533–536):
with , , chosen because it "gives
a couple of seconds of carry, which matches real side-spin persistence"
(cue_physics.c:55–57). This is an approximation: a real ball's vertical spin
decays through the same finite contact patch as its roll, and the coupling to the
horizontal spin components is neglected entirely.
A ball is airborne when (cue_phys_airborne, cue_physics.c:461).
In flight it gets gravity and nothing else — no cloth contact and no air drag
(cue_physics.c:2361–2363). The comment is explicit about why this matters: "Its
spin is preserved through the flight instead of being turned into roll, so it lands
still sliding and skids on, and that is why a jump shot behaves nothing like the same
stroke played flat. It is also why this must not happen by accident: a ball that hops
unintentionally stops developing roll, and every draw and follow shot quietly stops
meaning what it meant" (cue_physics.c:2330–2339).
What is under a ball is decided by surface_at() (cue_physics.c:1888–1932),
which returns one of three answers: (cloth), rail_top (cushion top and wood cap
as one flat level, cushion_h, cue_table.c:2874), or CUE_NO_SURFACE
(off the frame, or over a pocket). Landing on a surface applies bed restitution
(cue_physics.c:2367–2372):
is tuned so the last hop is under a millimetre: "below that it settles
flat rather than chatter, because every bounce suspends cloth friction and a ball that
never quite lands never develops roll" (cue_physics.c:70–76).
A guard prevents a ball that has penetrated a cushion (rather than climbed it) from
being teleported onto the rail: if the reported surface is above the cloth but the ball
is not yet clear of it, the surface reverts to the bed
(cue_physics.c:2347–2359). "Every hard shot does this: the ball penetrates the rail
by a millimetre or two before the collision resolves. Treating that as 'over the rail
region, so it must be on the rail' teleported it up onto the frame, from where it
rolled gently off the table and was deleted."
A ball off the cloth for more than 10 s of simulated time is removed as CUE_OFF_TABLE
(cue_physics.c:2400–2405, :2422–2427) — "a frame cannot wait on it: ten seconds and
it has left the table, which is what a referee would say."
Semi-implicit (symplectic) Euler: velocities are updated by the contact/friction terms
first, then
(cue_physics.c:2438–2441). On the cloth is then pinned to .
The substep is fixed at
driven by an accumulator from the frame dt (cue_phys_step, cue_physics.c:2609–2617):
w->_acc += dt;
while (w->_acc >= h && iters < CUE_MAX_SUB) { substep(...); w->_acc -= h; iters++; }
if (iters >= CUE_MAX_SUB) w->_acc = 0.0f; /* shed backlog */
with CUE_MAX_SUB = 400 (cue_physics.c:19). The rate is justified by the tunnelling
bound: "2 kHz keeps a fast break (≈6 m/s) to ~3 mm of travel per step — well under a
ball radius, so overlap-based collision resolution never tunnels"
(cue_physics.c:15–17).
cue_phys_set_substep() (cue_physics.c:2569) allows a coarser step for headless
ranking simulations; per the AI review the default is now the live 1/2000 (see §8).
Sleeping. A ball with ,
and skips the cloth and integration step entirely
(cue_physics.c:2433–2437); it is woken implicitly by any collision that gives it
velocity. "This is exact (a still ball doesn't move) and is the big win on this
low-drag cloth, where a shot's long roll-out leaves most balls stopped for ~100+
substeps each." When the whole table settles, cue_phys_step zeroes all of
and (cue_physics.c:2642–2653), for a measured reason:
"a resting ball with stored spin is not resting: ball-ball friction reads BOTH spins at
the next contact, so the residual bent the throw of the NEXT shot. Measured at 0.2 mm
of settle divergence from one quiet stroke into once-rolled balls."
cue_phys_strike_jump() (cue_physics.c:371–446) is the single entry point; the two
simpler forms are wrappers with and/or (:448–456).
Given aim direction (flattened, normalised), speed , tip offsets , elevation and bed-launch speed :
cue_physics.c:377, :387).cue_physics.c:403–405). is a setting (cue_phys_set_squirt,
cue_physics.c:364) with default 2.0°, range 0–5.0° (cue_physics.h:688–695).cue_physics.c:406–411).cue_physics.c:437–439).Hence , so the clamp at 0.50 gives a maximum of
1.25, "which is the real maximum" (cue_physics.c:428–439, cue_physics.h:866–879).
Two deliberate departures from naive modelling are recorded here.
cue_physics.c:388–401).cue_physics.c:413–426).There is no miscue model at all — a contact beyond the limit is simply held to it
(cue_physics.h:874–876). This is an explicit modelling choice, not an omission.
collide_ball_ball() (cue_physics.c:558–647). One resolution per pair per substep,
in a double loop over (cue_physics.c:2457–2495) with a per-axis broad-phase
reject at (the set's own diameter, safe because a smaller cue ball only shrinks
the pair sum, cue_physics.c:2453–2463).
The separation test is planar when both balls are on the cloth (identical , so
"the same test it always was for every shot that does not leave the bed — including
the arithmetic, which is why nothing that used to work moves by a bit",
cue_physics.c:559–566) and becomes a genuine 3-D sphere test when either is
airborne (cue_physics.c:569–578):
The overlap is split and each ball displaced along by
(cue_physics.c:587–590). The comment and the code disagree
here: the comment says "Split by SIZE, so a small cue ball is pushed further than
the object ball it met", but the code applies an equal split. Reported as
observed; the source does not explain the discrepancy.
With and separation early-out
(cue_physics.c:593–594):
Written with both masses because "English pool's cue ball is lighter than the balls it
strikes — 94 g against 116 — and with equal masses this is exactly the old m/2 reduced
mass" (cue_physics.c:596–600).
The relative surface velocity at the contact is formed from both spins
(cue_physics.c:607–612), with ,
:
The tangential effective inverse mass at a sphere contact is ,
independent of radius, so the pair gives (cue_physics.c:616–622):
This is a Coulomb cone with a stick branch: either the slip is arrested exactly, or
the friction saturates. Both linear and angular impulses are applied to both balls
(cue_physics.c:640–643). Throw and spin transfer therefore emerge rather than being
added on — as the header puts it, "ball–ball throw and english-off-the-cushion fall out
of the same framework rather than being faked" (cue_physics.h:19–21).
The direction is load-bearing, and the source records the bug and its measured cost in full:
"That inverts throw. Measured out of the engine (test_throw.c), a plain cut with no side threw the object ball up to 4 degrees off the ghost-ball line and in the WRONG DIRECTION — a cut was overcut where every real table undercuts. On a one-metre pot 4 degrees is 68 mm, which is most of a snooker pocket … It also made side unusable: turn it on and every sided shot missed by the throw." (
cue_physics.c:626–639)
Both balls' are zeroed after the resolve (cue_physics.c:645), so a ball–ball
contact is always planar in its outcome even when one ball is airborne.
There is no simultaneous solver and no manifold. Pairs are resolved one at a time,
in index order, once per substep. Repeated and near-simultaneous contacts (a break
pack) are handled purely by the substep rate: at 2 kHz a 6 m/s ball advances 3 mm per
step, so a chain of contacts is resolved over successive substeps rather than at once.
This is an approximation to a simultaneous impulse solve; the source does not claim
otherwise, and the tunnelling argument at cue_physics.c:15–17 is the whole
justification given.
This is the most elaborate model in the engine, and the one whose comments are the most useful.
The header comment on cushion_impact() (cue_physics.c:696–729) states the case:
"The cushion used to be handled by collide_surface above: one instantaneous Coulomb impulse against a tilted wall. That model is in a class which provably cannot do what a real cushion does. Biber, Champneys & Szalai (arXiv:2208.11685) show that a rigid contact with a Poisson or energetic restitution 'cannot capture slip reversal during the contact phase' — the slip direction is frozen at the instant of contact — and that the fix is to let the impact take TIME, during which the slip direction rotates."
The scheme implemented is Mathavan, Jackson & Parkin, A theoretical analysis of
billiard ball dynamics under cushion impacts, Proc IMechE Part C 224:1863 (2010),
eqs. 14a–14f, integrated over the accumulated normal impulse rather than over time.
The paper's own constants are quoted (, , ,
so ) and noted as being measured on a Riley Renaissance with snooker
balls, i.e. "this game's own sport rather than a generic sphere"
(cue_physics.c:705–717).
The critical structural point: two contacts act at once — the cushion at and
the cloth at — "and that second one is why the old model could not be fixed by
tuning: it was not there at all" (cue_physics.c:718–720).
collide_cushions() (cue_physics.c:941–1037):
rail_top is over the cushion, not bouncing off it
(cue_physics.c:956). The comment distinguishes this from cushion_nose and records
the failure: judging "cleared it" at the contact line "let a ball pass through 10 mm
of solid drawn cushion" (cue_physics.c:943–955).cue_physics.c:965–983). Each segment carries a face normal
n and two vertex normals na, nb; the collision normal is the interpolated
, giving "a continuous normal field along
the chain, so the ball sees a single shared normal across the rail↔facing junction
instead of bouncing off the kink" (cue_physics.c:960–964).cue_physics.c:985).The contact height is computed per impact, not taken from the table spec
(cue_physics.c:1003–1007):
so a ball already in the air meets the nose lower down, and once the contact is below
centre the same impulse points up. The reasoning is stated as geometry rather than as a
rule: "That falls out of the geometry rather than needing a rule"
(cue_physics.c:988–1002).
cush_sin is filled from the table: , with
cushion_h on almost every table — "63.5% of the ball, which is the WPA
specification" (cue_table.c:2843–2846, cue_table.c:66–68). It is stored as sine and
cosine, never as an angle, specifically to avoid a asinf/cosf/sinf round trip
through the platform's libm (cue_physics.h:195–203).
cushion_impact() (cue_physics.c:747–929) works in the paper's frame:
horizontal into the cushion ( flattened and
normalised),
along the cushion, world up (cue_physics.c:751–756). State is
projected: and
(cue_physics.c:758–765). , .
(cue_physics.c:784–786, :798.) The comment states the empirical basis and what
went wrong with a constant:
"A constant was measurably the wrong SHAPE, not merely the wrong number: square into the rail this model returned 89-90% of the speed at 2 m/s and the same 89-90% at 7, so a firm shot lost nothing extra and every angle off the rail played the same however hard it was hit. Mathavan's high-speed imaging of a snooker table has the rebound ratio at 0.910 for a ball barely moving and a 0.818 best fit across 0.28-3.5 m/s … and Marlow's rails come in near 0.55 — which is where a really firm one ends up. Linear in the approach speed, anchored on those two, floored so it cannot go absurd on a break." (
cue_physics.c:774–783)
Per-table values are set in cue_table_rails() (cue_table.c:76–109): US pool
K-66 worsted , ; UK 8-ball 0.965/0.052; Chinese 8-ball
0.968/0.050; carom K-55 on a heated table 0.990/0.038 ("the ball is meant to hold
speed through three rails, which no pool cushion is asked to do"); snooker
0.970/0.050; and one home table deliberately deader, 0.930/0.062. The block comment
records that these are inputs, tuned against measured output: "So they were tuned
against the MEASURED rebound, not set to the published figure and hoped for: snooker
lands on 83% at 2 m/s, 76% at 3.5 and 61% at 7, against Mathavan's 0.818 across normal
play and Marlow's 0.55 for a hard one" (cue_table.c:70–75).
If m/s the impact is not integrated at all; the normal velocity is
simply reflected, (cue_physics.c:798–803). "A BALL LEANING ON
THE CUSHION IS NOT AN IMPACT … running the full scheme there lets it settle a few
millimetres the wrong side of the cushion line in a mitred pocket corner, where the
segment normals fight each other."
Total normal impulse , taken in equal slices
(cue_physics.c:805–808). The loop is allowed up to
iterations (cue_physics.c:815); the slice count is justified as "96 lands within 0.1%
of 512" (cue_physics.c:731).
Per slice, the slip velocities at the two contacts are (cue_physics.c:817–822):
with slip angles and
— used only through their sine
and cosine, which are obtained algebraically as and (see
§7.2). Friction coefficients are gated on actual slip:
active iff , active iff the ball is on the
cloth () and (cue_physics.c:845–855, :790).
The cloth's share of the impulse (the paper's eq. 8) is
(cue_physics.c:857–860.) The clamp is the physical statement that a cloth cannot pull
downwards.
The per-slice increments (cue_physics.c:862–868), with :
is zero while the cloth holds () and becomes a lift when : "A
cloth cannot pull downwards, so when that goes negative the table has lost the ball and
the vertical velocity is free — the ball lifts. That is the real trigger, not a special
case bolted on" (cue_physics.c:722–728).
Termination is energetic (Stronge), not kinematic. The work done by the normal
impulse is accumulated trapezoidally,
(cue_physics.c:873–875). Compression ends the first slice at which ,
recording ; restitution ends when
(cue_physics.c:876–878) — i.e. the contact returns exactly of the compression
work. A second break guards a degenerate case where has gone to zero while still
closing.
Everything above lives inside the rigid-cushion assumption, in which the ball cannot
leave the table. Above m/s of normal approach the code adds a
climb (cue_physics.c:904–923):
The source labels this plainly, and it is the one place in the cushion model where the authors say they are outside the literature:
"This is EXTRAPOLATION, not measurement: nobody has published outgoing vertical velocities off a cushion, and this is a plain ramp in the excess over the rigid limit, scaled by how much the ball is rolling INTO the nose, which is the direction of climb." (
cue_physics.c:896–899)
The cap has a measured justification: "Measured: at the uncapped value, four shots in a
thousand ended up resting the wrong side of the cushion line"
(cue_physics.c:910–918). Finally is clamped — the cushion is never
allowed to drive a ball downward (cue_physics.c:927).
Jaw tips are immovable discs of radius jaw_r handled by collide_surface()
(cue_physics.c:1022–1035, :652–694), i.e. a single instantaneous impulse against
a horizontal normal:
with the angular impulse's vertical component scaled by
(cue_physics.c:688–690) so "the rail imparts much less NEW english … while the
horizontal (roll) axis keeps full friction so roll is correctly damped — no over-spin
build-up, and incoming english still bent the bounce above". Friction is skipped
entirely when m/s, because "a ball merely rolling ALONG the rail has a
near-zero approach speed and must not be braked every substep (that was the
'sticking')" (cue_physics.c:666–669).
The engine distinguishes three concentric-ish circles per pocket, all derived from the pocket point and the mouth normal :
(i) The bore / drop / catch circle. Centre
where is drop_back (corner) or
drop_back_side (middle), radius pocket_r[p] visible mouth cap_*
(cue_table.c:3184–3188, :2921). This one circle does four jobs, and the source is
emphatic that they must not diverge:
cue_physics.c:1737);surface_at — "THE HOLE IS THE CATCH. Same
centre, same radius, deliberately — these two are the only things that can disagree
about whether a ball is still on the table, and when they do it falls through the
world … Four per thousand on the 9 ft table went through it"
(cue_physics.c:1900–1908);jaw_yellow, cue_table.c:2216–2227);cue_table_clear_bore,
cue_table.c:3324–3349), after an octagon "put sixteen nose vertices up to 3.3 mm
inside the hole" (cue_table.c:3190–3205).cue_table_normalise() (cue_table.c:2784–2790) makes the timber's bore identical to
this circle by construction; its underside is cue_table_bore_bot() mm
(cue_table.c:4527).
(ii) The cut circle — where the slate and cloth actually end. Centre
with cut_set, radius
cut_ref cut_rad, where cut_ref is the visible
mouth (pr_corner/pr_side), not the drop radius (cue_table.c:4366–4370,
:3214). The comment records why they were separated: "rad used to be a multiple of
the DROP circle, which meant the cut could not be held still while the drop was moved"
(cue_table.c:4196–4200). The header adds the physical point: "at a middle it happens
well before the jaws, at a corner slightly after them" (cue_physics.h:358–366).
(iii) The lip roll, radius cut_ref cut_roll
(cue_table.c:4370), the quarter circle the cloth turns over the cut edge. A ball
riding it has its centre on an arc of radius . This radius is
capped so a ball can always reach the end of the roll before it reaches the deepest
point it can physically get to (cue_table.c:4398–4404), a fix with an unusually
precise motivation:
"On a Russian pyramid corner … the mouth is 72 mm for a 67 mm ball, and the deepest a ball can ever get is the middle of the pocket, where it was 0.9 MILLIMETRES short of the release. So it sat there. Dead centre in the pocket, at cloth height, held up by a roll it could not reach the end of, with the shot never settling and the frame unable to continue. 42 shots in 1890 on the 12 ft and 37 on the 7 ft." (
cue_table.c:4372–4397)
Per-table values of are tabulated in
cue_table_default_cut() (cue_table.c:4193–4325); the snooker corner is read off a
WPBSA 3MF gauge as "an arc r 3.5 in (88.94 mm fitted) CENTRED ON THE SLATE-EDGE CORNER"
(cue_table.c:4221–4231).
cue_phys_cut_out() — the cloth cut as a signed distance fieldcue_physics.c:1086–1108. Returns how far past the cloth edge a point is: negative on
slate, zero at the edge, positive out over the drop. The cut is "an arc around cut_c
with a straight tangent leg out to each slate edge — a quarter circle at a corner, a
half at a middle. That makes the distance to it exact and cheap: which of the three
pieces is nearest falls out of the sign of the point's two local coordinates, so there
is no search" (cue_physics.c:1078–1085).
Let , , , , (local coordinates with the table interior at ).
A hole in the open bed (bar billiards, pocket_bed[p]): the cut is a circle and
nothing else,
The reason is recorded: run through the legged form, "those legs claimed half the table
as 'no cloth here' — which is exactly the reported fault of pockets gathering balls
from nowhere near them" (cue_physics.h:325–333).
A middle pocket (half arc, two parallel legs), with unsigned:
A corner (quarter arc, two perpendicular legs):
The surface normal is taken numerically, by central differences of with
m (cue_physics.c:1193–1200, and again at :2164–2171), giving the
in-plane gradient direction "into the cut".
Observation, reported as read: this function branches on p >= 4 to decide "middle"
(cue_physics.c:1097), as does cue_phys_drop_walls (cue_physics.c:1179). That is
the index-based assumption the header explicitly warns against for L-shaped beds — "an
L has FIVE corners, so all three would have called its fifth corner a middle"
(cue_physics.h:343–348) — which the rest of the code answers with pocket_mid[].
The source does not reconcile the two.
Once flagged as dropping, a ball is stepped by cue_phys_drop_mesh() (drawn geometry
in the solver) or cue_phys_drop_fall() (gravity + drag + move), then constrained by
the lip (cue_physics.c:2108–2236) if there is no drawn lip mesh.
With and , the cloth surface height under the ball's centre is
(cue_physics.c:2137, :2162–2163.) The tangential direction points
down the roll; is the ball's speed along it. Contact is decided by height and
by whether gravity can hold the ball to the arc (cue_physics.c:2178):
This is the standard "ball leaves a convex surface when "
criterion, and it is what makes a firm pot look different from a dying one, as the
comment observes: "The roll only holds a ball that is slower than sqrt(g(ld+R)), about
0.7 m/s; anything above that leaves the edge and flies, and that falls out of the same
two lines rather than being asked for" (cue_physics.c:2104–2107; a second comment at
:2144–2146 quotes m/s for its own table).
While held, only the velocity component into the surface is removed
(cue_physics.c:2187–2192), and gravity rolls the ball down the slope at the
rolling-without-slipping rate (cue_physics.c:2209–2216):
The comment records a sign-and-share bug in this exact expression: "This had the sign
inverted and the wrong share — it applied 2/7 g sin(th) UP the roll, so a ball on the
lip was being braked by the very thing that should have been taking it down, while the
5/7 went into spin that nothing was coupled to. That is a ball sitting on the lip
turning over, on every table" (cue_physics.c:2199–2208). A related comment forbids
charging the ball twice for the rise: "the tangential gravity term above has ALREADY
taken that height out of its speed — charging again is counting it twice, and counting
it against the whole velocity is what deleted a ball's motion across the lip"
(cue_physics.c:2218–2226).
An earlier, discarded approach is documented in full at cue_physics.c:2150–2161
("WHY NOT 'vel.y = the roll's rate'"), including its symptom: "A slow ball toppled at a
tenth of gravity: 'balls on the lip fall in slow motion'."
A ball whose whole width is inside the throat wall is declared over open pocket
(over_open, cue_physics.c:2127–2129) and the lip constraint is skipped — "makes 'a
ball parked on top of a pocket' a shape the physics cannot express, rather than one it
merely happens not to reach."
check_pockets() (cue_physics.c:1718–1812):
rail_top is flying clean over and cannot be potted
(:1719).:1721–1731).:1737).:1771).The evolution of this test is one of the most instructive comments in the file
(cue_physics.c:1739–1770). A jaw-line half-plane test was tried and removed —
"a straight line has no ends. Combined with the proximity test it captured a HALF DISC,
and at a corner a ball hugging the rail crosses that line well to the side of the
mouth — pot area in FRONT of the pocket, which is the one place there must be none."
The circle alone was then validated: "1044 shots per table at every pocket, speed and
angle, and 16,632 more fired hard from twenty angles, all lose nothing with the circle
alone." And when a leak reappeared after re-tuning ("pushing the US table's catch 0.28 R
deeper put it behind where a hard ball crosses the throat, and four went off the back
of it"), the replacement was the test rather than a new rule.
On capture: pocket is recorded; with cue_phys_set_fast_pot on the ball is removed
immediately, otherwise drop = 1.0 starts the fall (cue_physics.c:1805–1806). The
fast-pot switch exists for the planner and carries its own measurement: "Measured on a
10 ft snooker table (2026-09-06): plans averaging 1373 ms with the fall in them"
(cue_physics.c:1786–1804).
Bed holes (bar billiards) are judged once, on the substep the centre crosses in
(cue_physics.c:1778–1782), by a free-fall argument in bed_hole()
(cue_physics.c:1660–1716). With hole radius , perpendicular miss distance
and chord half-length
:
with hole_catch (cue_physics.c:52). The whole character of the game is
derived from this one line: "you can pot the 200 from anywhere on the table, but only
at a pace that gives it time to drop, and thumping it just skips it across the hole"
(cue_physics.c:1654–1657). A rim-out is given a restitution against
the far lip, horizontal damping and an upward kick , with
(cue_physics.c:1696–1713). The whole chord is used, not
the remainder, and the reason is a determinism argument: "the live game samples at 2 kHz
and the AI ranks shots at 600 Hz, so the AI would have judged the same shot harder than
it is and stopped playing pots that go in" (cue_physics.c:1677–1686).
cue_phys_drop_walls() (cue_physics.c:1153–1282) supplies analytic walls where a
table has no drawn geometry (the handheld, the tests): the cut's vertical face through
the bed (using and its numerical gradient), the pocket's iron bridge, and a
funnel "bag" whose radius interpolates linearly from the cut's reach at the mouth to
at a ring below (cue_phys_bag_r, cue_physics.c:1144–1150).
The iron does not play the ball back; it turns horizontal speed into fall
(iron_down, cue_physics.c:1395–1400) with an explicit energy cap:
The reason is a measured energy leak: "Written as vy -= k*vu that quietly created
energy whenever the ball was already falling — the two add, and the square of the sum
is more than the sum of the squares. On a pool table … the pocket test caught it fifteen
times and up to 1.75 J/kg, which is 1.9 m/s conjured out of nothing"
(cue_physics.c:1387–1394).
Where the app supplies drawn meshes, cue_phys_drop_mesh() (cue_physics.c:1436–1516)
hands the ball to the general rigid-body solver against every static surface under the
cloth at once — the lip, the return box, and each pocket's solid and net
(cue_phys_under_bodies, :1295–1319) — with materials: leather over iron
, ; net , ; moulded return , ;
cloth lip , (cue_physics.c:1284, :1307–1316). The lip's very low
friction is itself a documented finding: "At the solver's 0.22 the lip was a rubber
gripper: the friction solve drives the contact's slip to zero every substep, so the
ball was spun up to match the speed it gained going down, which is a gear and not a
pot" (cue_physics.c:1321–1330).
Cushions are a chain of CueSeg records — endpoints , face normal , the two
vertex normals , and a kind flag (0 rail nose, 1 pocket facing/jaw)
(cue_physics.h:171). Construction (cue_table.c):
mitre_kiss, cue_table.c:1521–1541): the jaw circle is placed
as the incircle of the corner — centre on the bisector at , each
straight trimmed back to its tangent point at , with the tangent
distance capped at of the shorter leg. The arc between the two tangent points is
emitted as 3–6 segments of about 1 mm each (add_arc_between, cue_table.c:1547–1565).
The motivation is a measurement: "Measured on the 9 ft American middle: the circle sat
8.3 mm behind the knuckle and a ball walked straight at it stopped 33-37 mm away
against a 32.6 mm contact radius. It never once registered, and what the ball met
instead was the FACING, whose normal looks into the mouth — so a ball onto the point
was deflected IN rather than thrown out" (cue_table.c:1499–1520). Crucially, "THE FACE
LINES DO NOT MOVE. Only their ENDS do."jaw_curve, cue_table.c:2238–2256) are cubic Béziers
with both ends pinned: is the yellow point — where the bore circle crosses the
frame's inner face along the rail direction, solved as a ray–circle intersection taking
the far root (jaw_yellow, cue_table.c:2216–2227) — sits jaw_p0 along the
rail from 's foot on the cushion face, and the two control points slide along the
rail tangent () and the pocket's axis tangent (), the latter rotated off the
pocket centre line by jaw_ang_c/jaw_ang_m (jaw_tangent, cue_table.c:2358–2365;
jaw_end, cue_table.c:2377–2438). CUE_JAW_SEGS is 3 on the handheld
(cue_table.c:18) and 10 in CueVR (CueVR/CMakeLists.txt:87).smooth_seg_normals, cue_table.c:2792–2828): vertex normals are
averaged across shared endpoints, but only within a run of the same kind and only
when the two face normals agree to within (≈37°). Both restrictions
have recorded failure modes: averaging across a sharp convex mitre means "a ball gliding
along the rail bounces off 'the back of the knuckle' instead of glancing off the flat
rail"; averaging a facing into a rail nose meant "a ball hugging the rail near the
knuckle saw a normal tilted toward the pocket throat — funnelling it into the pocket and
spinning it up off the rail."cue_table.c:3157–3182): each pocket's mouth is the midpoint of
the two nearest jaw tips and is the normal of that line, oriented
away from the table centre. cue_table_mouth_at() (cue_table.c:3409–3469) then
measures the actual narrowest passage as a min over segment–segment, circle–circle and
circle–segment distances, restricted to features that own that pocket (nearest-pocket
assignment).A dropping ball still meets the jaws and cushions while any of it is above the cloth,
with its effective radius shrunk to the chord at its current depth,
, "so it slips under the noses the way a ball does and can
never leave through the side of a cushion" (cue_physics.c:2267–2301). The vertical
velocity is explicitly preserved across that call, because collide_surface zeroes it
(cue_physics.c:2280–2292).
mote_phys.c)A general sequential-impulse rigid-body solver with warm starting. It runs the bar
billiards skittles and everything below the cloth. Header summary: "Sequential-impulse
solver with WARM-STARTING … Warm-starting is what lets tall stacks stay stable in a
handful of iterations — without it, Gauss-Seidel propagates floor support only one body
per iteration and deep stacks sink/collapse" (mote_phys.c:1–13).
| Constant | Value | Source |
|---|---|---|
DEFAULT_H |
1/240 s (per-world overridable; CueVR sets ) | mote_phys.c:27, cue_physics.c:234 |
SOLVER_ITERS |
8 velocity iterations per substep | mote_phys.c:29 |
POS_ITERS |
4 position iterations | mote_phys.c:30 |
MAX_SUBSTEPS |
8 (CueVR/ThumbyCue: 400) | mote_phys.c:31, cue_physics.c:235 |
REST_SLOP |
0.45 m/s — below this | mote_phys.c:28 |
POS_BETA |
0.2 Baumgarte factor | mote_phys.c:32 |
POS_SLOP |
5 mm allowed penetration, but never more than POS_SLOP_REL=2% of the body |
mote_phys.c:33–35, :915–919 |
WAKE_PEN / WAKE_VEL |
0.02 m / 0.30 m/s | mote_phys.c:36–38 |
SLEEP_DIST2/ANG2/VEL2/FRAMES |
25 mm, 0.40 rad/s, 0.02 m/s, 20 substeps | mote_phys.c:39–47 |
SLEEP_LEVEL_COS2 |
mote_phys.c:64 |
No inertia tensor is stored; the world-space inverse inertia is a body-frame diagonal
rotated into world (iinv_mul, mote_phys.c:95–115):
cue_physics.c:1608–1617).Broad phase is a uniform grid when bodies (cell size , grown
until the cell count fits), else all-pairs
(mote_phys.c:951–1029, :1084). Static planes and static meshes are excluded from the
grid and tested against every awake dynamic body each substep
(mote_phys.c:1101–1108). Mesh colliders use a 2-D XZ grid, testing the 3×3 cells
around the body (gen_vs_mesh, mote_phys.c:698–723).
Narrow phase dispatch (gen_pair, mote_phys.c:741–756):
| Pair | Method | Contacts |
|---|---|---|
| sphere–sphere | analytic | 1 (:210–218) |
| sphere–box | nearest point on OBB, with a deepest-axis fallback when the centre is inside | 1 (:318–339) |
| box–box | SAT over 15 axes + reference-face clipping | up to 4 (:251–315) |
| box/hull–box/hull | Cvx expansion + face_query SAT + incident-face clip |
up to ~4 (:578–632) |
| anything with a capsule (round) | GJK + EPA | 1 (:729–739) |
| vs plane | per-shape: sphere point, capsule two ends, hull every vertex, box eight corners | many (:759–791) |
| vs mesh | per triangle; faceted bodies get a clipped manifold, round bodies a face-normal point | many (:662–695) |
Each contact stores a warm-start key hashed from the two body indices and a feature
id (add_contact, mote_phys.c:196–205).
The per-shape plane test is not incidental: the skittle world uses real static planes
rather than the solver's own bounding-box walls because "those test a hull as a box of
its half extent — which a hull does not set, so the box is a point at the centre of
mass and a pin sinks to its own middle before anything stops it. gen_vs_plane tests a
hull vertex by vertex, which is the shape actually being asked about"
(cue_physics.h:495–501).
prepare() (mote_phys.c:829–861) precomputes, per contact:
eff_mass, :817–827);mote_phys.c:848):
, ; a zero per-body value falls back to the
world's;REST_SLOP, else
(:850–852) — this is the anti-jitter gate;:854–858).solve_vel() (mote_phys.c:863–883), run SOLVER_ITERS times, is textbook projected
Gauss–Seidel:
with only the increment applied each time, so the accumulated impulse is what is clamped.
The friction cone is reduced to a single direction. There is one tangent
per contact, fixed at prepare() time from the initial slip, and one
scalar clamped to . This is neither a two-direction pyramid nor a true
cone projection; it is exact when the slip direction does not rotate during the solve and
approximate otherwise. The source does not discuss this choice.
Penetration is removed through a separate pseudo-velocity field that integrates into
positions only (mote_phys.c:889–942): "it never touches real velocity, so deep
simultaneous landings can't explode the way Baumgarte-in-velocity does."
with . After POS_ITERS sweeps, positions and orientations are advanced by
and and the orientation
re-orthonormalised (mote_phys.c:1118–1131).
The slop is relative to the body, and this is a measured fix
(slop_for, mote_phys.c:905–919):
"A flat POS_SLOP is a fraction of a crate and a fifth of a snooker ball, and what it costs is not the look of it: the contact point is on the surface, so a ball sunk 5 mm of its 25.4 mm is spun by friction about a 20 mm arm rather than its own radius. Measured on a bare mesh slope, that is 28% too much spin for the speed and 60% of the (5/7)g sin(theta) a rolling ball is owed — enough that a ball trickling onto a pocket lip stopped dead and sat there for nearly three seconds."
integrate() (mote_phys.c:1066–1077): semi-implicit Euler with implicit linear and
angular damping,
then a world-axis rotation of the orientation and m3_orthonormalize.
A body sleeps only when it is in contact, not deeply overlapping, barely displaced,
barely rotating and barely moving, for SLEEP_FRAMES=20 consecutive substeps
(mote_phys.c:1210–1212):
The velocity term is itself a recorded fix: "Displacement and spin alone let a ball that
had just landed on a slope — moving, not yet turning, still inside 25 mm of where it
touched down — fall asleep after twenty substeps and stay there for ever … Two
centimetres a second is not at rest" (mote_phys.c:41–46).
The level-support rule applies to spheres only (mote_phys.c:1191–1201). Let
be the total contact impulse on body and its
exponential moving average with (mote_phys.c:1189–1190). Then
"A ball has nothing to hold it on a slope — it rolls — so it may sleep only when the net
normal impulse on it points straight against gravity, within half a degree: a level
floor, a groove, a hollow. Boxes, capsules and hulls keep the old rule; they have faces
and friction to rest on" (mote_phys.c:1139–1147).
Two things had to be right for this to be reachable, and both are documented as bugs found in play:
mote_phys.c:1155–1172). The summed vector is
, "the same vector apply_impulse
uses, so the two cannot drift apart."mote_phys.c:1180–1188). A moving
body resets the average rather than carrying it (:1231–1234).The threshold itself is derived rather than tuned: "A ball coasting through zero on a
slope of angle T is held by an impulse tilted from vertical by atan((5/7) tan T) — 0.536
degrees on the ball return's three-quarter-degree gully. That is the case this test
exists to reject, so the gate must be well inside it, and a quarter of a degree is"
(mote_phys.c:48–64).
Sleeping bodies act as immovable anchors: eff_im() returns 0 and iinv_mul()
returns zero for them (mote_phys.c:92, :96), "so an awake body resting on them can't
push them around (which used to collapse stacks)." They wake on deep intrusion
(WAKE_PEN) or a fast approach (WAKE_VEL) (wake_check, mote_phys.c:985–994), or on
being displaced past SLEEP_DIST2 (:1205–1209).
Each bar billiards pin is a convex hull of revolution — foot centre, foot rim
(1.62 stem radii at 0 height), cap rim (3.40 radii at 97 mm), top — built once by
skittle_hull_build() (cue_physics.c:118–193). The choice is explained: a stem-width
capsule "is not the shape the CLOTH meets. A mushroom lying down rests on the rim of its
cap and the flare of its foot, and a stem-width capsule has neither: the pin lay down
with its head buried in the bed" (cue_physics.c:94–108). The ball, however, is tested
against the stem capsule (check_skittles, cue_physics.c:1564–1586), because
Rule 74 keeps every ball on the plain cylinder.
The ball–pin impulse is resolved by hand (cue_physics.c:1593–1631) with
and the effective inertia taken from mote's own hull rule, so the two solvers
cannot disagree. A pin is down when its own axis has left vertical by more than
(cue_physics.c:2031), and nudged when it ends more than
1.5 mm off its spot (:2041–2044) — "That used to be a speed threshold at the moment of
contact, which was a guess standing in for an outcome; now the body has an outcome and it
can simply be read."
The pin world runs on the same clock as the balls, with 400 substeps,
"so a pin and a ball cannot disagree about when a contact happened"
(cue_physics.c:232–235).
Mode A — two controllers (cuevr_cue.c:2055–2286). The bridge is a rigid offset in
the bridge controller's own frame:
(cuevr_cue.c:2104–2105, :2117–2128). The offset is in the controller frame, not the
room's, for a reason recorded in full: "It used to be a world-space vector added to the
controller, which is right only while you stand where you were standing when you set it.
Walk round to the other side of the table and 'five centimetres that way' still means the
same direction in the ROOM" (cuevr_cue.c:2084–2103). The minimum hand separation is
5 cm (:2118), lowered from 10 cm because "Some players hold both controllers out in
front of them, close together like a trombone".
The stroke is armed by the grip-hand trigger with wide hysteresis, 0.55 up / 0.15
down (cuevr_cue.c:2303). The reason is worth quoting because it is a measurement
problem, not an input problem:
"A single threshold is what made the power a lottery. An index trigger is analogue and your whole arm is moving during a delivery, so finger pressure dips — and one dip below the threshold disarmed the stroke, the next frame re-armed it, and re-arming resets the sample window, the clock and the locked tip. The measurement restarted from nothing with a one-frame baseline, at a random point in the delivery. And it was invisible." (
cuevr_cue.c:2288–2302)
On arming, the line is locked: lock_axis, lock_butt0 (the grip hand's position),
lock_tip0 (cuevr_cue.c:2318–2350). Thereafter the tip is a point sliding along that
fixed line by the butt hand's travel along it:
Mode B — a rigid tracked cue (cuevr_cue.c:1685–2053), a real stick with a
controller clamped to it:
(cuevr_cue.c:1704–1705). There is no trigger at all: "A real cue plays the ball when
the tip reaches it" (cuevr_cue.c:2037–2045); a stroke re-arms when the tip pulls more
than 50 mm off the ball (:1911–1914). Three attempts to filter or predict this line are
recorded as having been tried and reverted: "each made the drawn cue lag the real one and
drift out of step with it, which is worse than the coning they were meant to fix. The cue
is where the controller says it is, every frame" (cuevr_cue.c:1696–1703).
The distinction between the two modes is not cosmetic; it changes what the pose ring is
allowed to reconstruct (cuevr_cue.c:1412–1429):
"A REAL CUE is a rigid body clamped to one controller: rotate the controller and the tip swings a metre and a half away, which is most of where its speed comes from. The ring rebuilds it by rotating a fixed offset, and that is exact. A CUE HELD IN TWO HANDS IS NOT THAT. Its line comes from the two hand POSITIONS — grip through bridge — so turning your wrist does not move the tip at all, and rebuilding it as a rigid body on the butt controller would invent up to 1.5 m x omega of tip travel out of a turn of the wrist."
Accordingly CueVrRingCue carries a slide flag (cuevr_cue.c:1430–1439): rigid mode
rotates a fixed offset, slide mode projects the hand's motion onto the locked line.
The problem statement is at mote_xr.h:30–43 and mote_xr.c:1159–1179: at 72 Hz a frame
is 13.9 ms, which is 139 mm of tip travel at 10 m/s against a 52 mm ball. Before building
a sampler thread, the authors measured whether extra samples would carry new information
(MOTE_XR_TRACKPROBE, mote_xr.c:1180–1296), testing two things: how often the pose
changes when polled at 500 Hz, and how far a sample sits off the chord joining its
neighbours one frame either side. The answer is recorded in the header:
"Measured on a Quest Pro (2026-09-04), the runtime answers xrLocateSpace with GENUINELY NEW poses at about 480 Hz — 0.4% of samples 2 ms apart were collinear with their neighbours, where linear interpolation between 72 Hz updates would have made six in seven of them exactly collinear." (
mote_xr.h:35–41)
The ring (mote_xr.c:1300–1368) is a detached pthread on a 2 ms nanosleep, writing both
hands into a 1024-slot ring (~2 s). It is single-writer / many-reader: the head is
published with a release store and read with an acquire load, the reader copies backwards
and then re-reads the head to detect a lap (mote_xr.c:1354–1368).
Each sample carries the pose and the runtime's own velocity, chained onto the same
xrLocateSpace call as an XrSpaceVelocity (mote_xr.c:1329–1344). The reason is a
measurement of exactly the failure mode this avoids:
"A position stream can be smoothed — by the fusion, by the pose predictor, by anything between the sensors and us — and differencing a smoothed signal gives a smoothed speed with the peaks taken out of it. A cue delivery is nothing but a peak. Measured in a headset: a player swinging as hard as they could produced 12 mm of tip travel in 2.1 ms however hard they swung, and two very different strokes both scored 5.7 m/s." (
mote_xr.h:47–61)
The tip's velocity from a sample is then in
rigid mode, or the component of along the locked line in slide mode
(cuevr_cue.c:1464–1471).
hr_track() (cuevr_cue.c:1441–1549) consumes every ring sample newer than the last one
taken, oldest first, and maintains two histories of length CUEVR_CUE_HIST=128
(cuevr.h:456–462): a filtered one (drawn) and a raw one (played). The filter is a single
pole with the sample's own interval, , applied to the axis and
to the across-line component of the tip's motion only:
(cuevr_cue.c:1482–1489). "MOVEMENT ALONG THE CUE IS NEVER FILTERED, at any setting. That
is the delivery, and low-passing it would eat the power. Only the direction and the tip's
movement ACROSS its own line are smoothed, which is the tremble"
(cuevr_cue.c:1403–1405). ms
(cuevr_cue.c:1973–1976).
A single filter running on the 500 Hz data is itself a fix: "This used to low-pass the
FRAME's pose at frame rate while the strike walked the raw ring underneath it: two cues,
and on the heavier settings they part company by as much as the filter's own lag the
moment the stick moves. So the player aimed with one line and played the ball with
another" (cuevr_cue.c:1954–1972).
Four tip speeds are recorded per frame (cuevr_cue.c:1493–1535): the per-sample rate, a
4-sample mean (hr_tipv_sm, "a single sample is mostly jitter"), a ~20 ms chord
(hr_tipv20, immune to sampler jitter by construction), and the runtime-fused figure
(hr_tipv_rt). The hand's own speed is kept alongside for diagnosis: "if the tip is
barely moving faster than the controller, the cue is being carried rather than swung"
(cuevr_cue.c:1512–1518).
hr_contact() (cuevr_cue.c:1561–1602) walks the raw history — "The filter is for
the eye: it shortens every step by the across-the-line part, so a stroke measured off it
is measured short, and the power goes with it" (:1566–1569) — from the oldest sample not
yet judged, testing each consecutive pair as a segment against the ball.
cuevr_seg_ball_hit() (cuevr_hit.c:4–25) is a segment–sphere test. With
, the segment direction, its length
and :
The crossing yields the speed , the hit point
, and the interpolated cue axis at that instant
— "The tip's PATH and the cue's AXIS are
different things, and it is the axis the ball leaves along: a cue is a rod, so the impulse
runs down the shaft" (cuevr_cue.c:1588–1593).
The ring's answer is subject to one sanity gate (hr_apply, cuevr_cue.c:1660–1667):
it is discarded if it disagrees with the frame's axis by more than
. "A pose ring is a lock-free ring written by another thread,
and one torn sample would otherwise send a ball off at any angle it liked. Twenty degrees
between the ring's line and the frame's is 1400 degrees a second at 72 Hz: no stroke does
that. It is a guard against a bad read, not a limit on what a player may do."
The frame path is retained as a fallback and covers three cases, in
cuevr_cue_update()'s tail:
cuevr_cue.c:2840–3132): the frame's own geometry is
computed from the axis–sphere chord, and the ring's answer overrides it when there was
a crossing.along\_axis < -\varrho, cuevr_cue.c:2711–2779): ask
the ring first, because at speed this is the aftermath of a strike rather than a
shaft leaning on a ball. The measurement that forced this: "measured in a headset at a
tip doing 7.07 m/s, the ball got 1.23, with no side, no screw and no elevation — the
signature of this branch. The harder the stroke the likelier it is, which is why the top
of the power range was both weak and unpredictable" (cuevr_cue.c:2712–2727). If the
ring saw nothing, it really is a shaft push: speed is the closing rate of the
perpendicular distance, capped at tip_v, with — "That is a foul, which
is what it is on a real table, rather than a shot the player never played"
(cuevr_cue.c:2698–2777).cuevr_cue.c:2782–2838): the ring is the only
answer there is, capped at tip_v.The whole reason the ring exists for the two-controller mode is stated with the number:
"at 8 m/s it jumped 111 mm between looks and stepped straight over a 52 mm ball — 'the
cuetip just goes through it and eventually side hits it'" (cuevr_cue.c:2631–2644).
Three estimators are computed and combined, in cuevr_cue.c:2884–3095.
(a) The gap history. gap is the axial distance from the tip's surface to the ball's,
sampled once per frame into a 12-deep ring with cumulative times
(CUEVR_SPEED_N=12, cuevr.h:255, cuevr_cue.c:2918–2925). The speed at contact is a
three-point backward difference, exact for constant acceleration
(cuevr_cue.c:2974–2996):
used only when (the whole span is forward motion), with a first-difference fallback otherwise. The motivation is a bias analysis:
"A trailing average — over the delivery, or over a fixed window — is biased low by however much the cue accelerated inside it, and that bias grows with the stroke: soft shots came out right and hard ones came out at three quarters. Every earlier test missed it because they all delivered at constant velocity, which has no acceleration to be biased by and measures identically over any baseline." (
cuevr_cue.c:2959–2973)
An earlier design note explains why per-frame speeds are not averaged: "a predicted
display time that lands a millisecond early, or a dropped frame that reports one
interval's dt for two intervals' motion, and the same physical stroke reads as a tap or a
smash" (cuevr_cue.c:2899–2917).
(b) The ring's crossing speed, from hr_contact (§6.3), capped at tip_v.
(c) tip_v — a peak-tracking estimate of the tip's own speed
(cuevr_cue.c:2677–2695), fed from the smoothed ring reading where available and from
the drawn tip otherwise, held at the peak and decayed toward the current value.
"A real delivery accelerates, so an averaged speed lags the arrival and a ceiling built on
it would dock power off exactly the hard shots."
The final reading is (c). cuevr_cue.c:3091–3093: sp = c->tip_v when available.
The comment records the headset measurements that forced this and abandoned the gap as the
primary reading (cuevr_cue.c:3061–3085):
"three deliveries of the same pace: ring 5.60 gap 23.18 over ONE frame -> the ceiling won -> 11.70; ring 5.35 gap 4.68 over four -> the gap won -> 6.05; ring 5.00 gap 5.34 over four -> the gap won -> 5.65. Twice the ball for the same stroke, decided by where the baseline fell. 'Sometimes going harder led to slower shots', reported."
Stroke gain. The measured tip speed is then remapped
(stroke_gain, cuevr_cue.c:1645–1651):
This is an explicit, non-physical range remap, and the source says so and gives the fit
data (cuevr_cue.c:1604–1644): a person in a headset spans "3.9 m/s at the tip for a
gentle shot and 7.1 for everything they had", i.e. about 1.9:1, where the table wants 3:1
(a 4 m/s roll-up against a 12 m/s break). It is quadratic so the slope at the knee is
exactly 1 "and there is no edge to feel going over it". The limits are stated honestly:
"WHAT IT CANNOT DO is separate strokes that are the same. Four deliveries the player
described as differently hard measured 4.70, 5.14, 5.19 and 5.45."
Firing condition (cuevr_cue.c:3056): rest_armed && speed_n >= 2 && gap <= 0 &&
speed > 0.12. A former glitch guard was removed at the player's instruction: "A break is
exactly that shape — a still address, then everything at once — and at 12 m/s a frame is
167 mm, so a hold is a shot that fires late into the follow-through and a refusal is no
shot at all" (cuevr_cue.c:3013–3022).
cuevr_strike_geom() (cuevr_hit.c:27–58) builds a frame from the cue axis and world up:
with the aim direction the flattened, normalised axis.
The divisor is , not , and the consequence of getting it
wrong is documented in detail (cuevr_cue.c:2851–2865): "off runs from the ball's centre
to the point where the TIP's surface meets it, so its length is R + the tip's radius — and
dividing that by R scaled every offset up by 19% … a merely off-centre contact gave the
ball full spin and no speed at all — the tip visibly striking and the ball barely leaving."
The tip radius is a player setting, quoted in millimetres across, with named presets:
8–9 mm English pool, 9.5–10 snooker, 11–12 carom, 12–13 US pool, 14 break cue
(cuevr_app.c:10739–10751).
There is no miscue and no pace penalty for striking off centre
(cuevr_cue.c:3117–3130): "There was a cliff here — past 0.55 of the radius the pace was
cut to a flat 35% — and then a gentler version of the same idea. Both were wrong. Half a
ball of side is an ordinary shot, and a hard step is a poor model of anything physical even
where the effect is real."
then go straight into
cue_phys_strike_jump() (§1.6), where the offset is clamped to
and converted to spin at .
cuevr_app.c:22501–22512:
with kg (18 oz) and ("leather on phenolic"),
cuevr_app.c:135–136, and MAX_STRIKE_SPEED = 12 m/s (cuevr_app.c:114). This is the
one-dimensional elastic-collision result for a cue that is not slowed by the player's grip.
The comment states the effect: "A cue is heavier than a ball (~0.55 kg against 0.16) and
the contact is fairly elastic, so for a centre-ball hit the ball departs at roughly 1.35
times the tip's speed. Passing the tip speed straight through as the ball's — which is what
it did — under-reads every shot by about a third."
power_trim is a player taste setting, 0.50–1.20, and the UI labels it as such:
"The physics of tip-on-ball is not adjustable and is not what this is: it is how much of
your arm's speed was you meaning it, and the honest answer depends on the room, the
controller and the player" (cuevr_app.c:10752–10765, cuevr_cue.c:188–192).
Jump launch (cuevr_app.c:13375–13380):
It is a deadband, not a scale, and the reason is a physics-fidelity argument rather
than a gameplay one: "a hop of a fraction of a millimetre would still suspend cloth
friction for its whole flight, and cloth friction is where draw, follow and stun come
from. Losing that intermittently, on an elevation difference nobody can see, would make
every shot in the game feel unreliable" (cuevr_app.c:13345–13354). Subtracting rather
than testing keeps it continuous. Notably, cue_phys_strike_jump requires the caller to
compute , partly for this reason and partly for lockstep — "Two machines that each
derive vy from their own reconstruction of min_elev can disagree about whether the ball
left the bed at all, which is not a divergence any amount of position correction repairs"
(cue_physics.h:646–662).
cue_physics.c:2609–2617). The rate is chosen from the tunnelling bound, not from
stability (cue_physics.c:15–17).mote_phys.c:1088–1090) with 8 velocity
and 4 position iterations per substep; CueVR configures it to the same so that the
two clocks agree.mote_phys_step ends with
if (w->_acc > h) w->_acc = 0.0f (mote_phys.c:1241), and the CueVR drop path works
around a related float artefact by making four separate quarter-step calls rather than
one call with substep = h/4:
"mote's accumulator subtracts the substep from the step and tests >=, and in float
h - 3*(h/4) comes up a hair short of h/4, so the fourth substep never ran and the
leftover was thrown away with this fresh world. Every dropping ball moved at three
quarters of its speed under three quarters of gravity, on every build since the mesh
pockets: 'balls on the lip fall in slow motion'" (cue_physics.c:1442–1456). This is a
known engine quirk documented at the call site rather than fixed in the engine.cue_physics.c:1442–1446),
because "At 6 m/s a ball moves 3 mm in the sim's 0.5 ms; lifted by its own spin against
the back it can bury itself in the plate's chamfer between two steps, and the solver's
correction then throws it out at several metres a second."Online play is lockstep on the strike, not on the outcome: "Both machines integrate
the same 2 kHz physics from the same state, so the same six numbers produce the same table
on both sides" (cuevr_app.c:22608–22610). The wire packet carries direction, speed, side,
vertical, elevation and the player's own squirt setting (cuevr_app.c:22612–22620). This
places a hard bit-reproducibility requirement on the integrator, and three separate
measures serve it.
(a) No platform libm in the hot path. cue_trig.h supplies cue_sincosf /
cue_sinf / cue_cosf built from quadrant reduction plus Taylor cores to /,
in double, rounded once — "ONLY + - * / AND CONVERSION, every one of which IEEE-754 rounds
exactly, so the answer is a function of the input and nothing else." The reasoning is
stated with its measurement:
"Two headsets run the same binary, so every arithmetic instruction agrees, but libm is part of the OS image: a Quest 2 and a Quest 3 are on different Android builds, and those are allowed to differ in the last place. A power shot amplifies a last-place difference by about a thousand — on a UK 8-ball break, moving the cue ball 100 nm moves the worst ball 0.001 mm at 2 m/s and 0.372 mm at 12, and a tenth of a millimetre in comes out a quarter of a METRE." (
cue_trig.h:1–13)
The caveat is also stated: the compiler may contract a*b+c into an FMA differently
between the arm64 device build and the x86-64 host, so host-to-device bit equality would
need -ffp-contract=off; device-to-device equality is unaffected.
(b) The trig was removed algebraically where it could be. In cushion_impact the
slip angles are never formed. The original took atan2f of each pair and immediately the
cosine and sine of the answer; the replacement uses and
with already computed:
"Six libm calls a slice, 576 slices a cushion contact, to arrive back at the two numbers that went in. It is not only wasted work, it is the game's whole exposure to platform floating point … That is a Quest 2 and a Quest 3 disagreeing about whether a ball went down — reported from play, as 'only on power shots'." (
cue_physics.c:823–844)
(c) No allocation in the per-shot record. The cue ball's contact log is a fixed
CUE_MAX_TOUCH=24 array with an overflow flag, "because the match is lockstep, so this
must fill identically on both machines with no allocation" (cue_physics.h:429–450).
The cush_sin/cush_cos storage decision (§0) belongs to the same family: "It was stored
as radians, set with asinf() and read back with cosf() and sinf() in the same breath — a
round trip through the platform's libm to recover the number that went in"
(cue_physics.h:195–203).
CUE_HOT places substep() and its callees in .ramtext on the RP2350 device build,
copied to SRAM at load, because "The Mote module's code executes from XIP flash by
default, which is markedly slower than the native build's hot path"
(cue_physics.c:21–30).cue_physics.c:2429–2437)
and are excluded from the cushion/pocket scan (cue_physics.c:2502–2503).cue_physics.c:2461–2463), for the snooker case ().att_track gates the referee's per-substep instrumentation off for the AI's ranking
worlds; the comment corrects an earlier, wrong attribution of cost: "An early version of
this comment blamed the log for test_ai_frames' running time; measured properly, that test
is simply slow … and was as slow before the log existed. The gate stays because it is
right, not because it was the cost" (cue_physics.h:417–427).These are the places the source explicitly says it is not modelling the physics, and why.
| Departure | Where | Stated reason |
|---|---|---|
| No pace penalty for an off-centre strike (no ) | cue_physics.c:388–401 |
"true of the impulse but wrong about the outcome" — a ball struck near its edge leaves at nearly the same speed |
| No miscue; the tip offset is clamped | cue_physics.h:874–876, cuevr_cue.c:3117–3130 |
"These are good cues with chalk on them: a tip does not slide off" |
| Cushion lift above 2.5 m/s | cue_physics.c:881–923 |
"This is EXTRAPOLATION, not measurement: nobody has published outgoing vertical velocities off a cushion" |
| Hop capped at 0.55 of the nose height | cue_physics.c:910–921 |
measured: "at the uncapped value, four shots in a thousand ended up resting the wrong side of the cushion line" |
| Vertical spin from the rail scaled by ; the linear friction impulse is not | cue_physics.c:684–690 |
asymmetric on purpose: less new english, but incoming english still bends the bounce |
| Skittles topple rather than rebound the ball elastically | cue_physics.h:452–467, cue_physics.c:1537–1547 |
"a ball that reaches one topples it and carries on, and pretending it is a post to rebound from would be a worse lie than ignoring the deflection" |
stroke_gain remapping of human arm speed |
cuevr_cue.c:1604–1651 |
a person spans 1.9:1, the table needs 3:1 |
| Jump launch is a deadband, not a scale | cuevr_app.c:13345–13354 |
a sub-millimetre hop would suspend cloth friction and silently break draw/follow |
| The frame's top is a floor a ball can run along | cue_physics.c:1870–1886, cue_physics.h:251–260 |
"Removing it the instant it passed the cushion line … deletes a shot that is still happening" |
| A ball off the cloth for 10 s is removed | cue_physics.c:2400–2405 |
"a frame cannot wait on it" |
iron_down caps the fall it produces at what the wall absorbed |
cue_physics.c:1387–1400 |
the naive form created energy; measured at up to 1.75 J/kg |
| Rolling on the pocket lip charges no extra energy for the rise | cue_physics.c:2218–2226 |
the tangential gravity term already accounts for it |
| The AI does not adjudicate pots by simulation | cue_ai.c:6089–6107 |
"letting a perfect simulation adjudicate it makes the opponent superhuman" |
Reported as read, without speculation about intent:
collide_ball_ball splits the de-overlap equally
(cue_physics.c:587–590) while its own comment says it splits by ball size.cue_phys_cut_out and cue_phys_drop_walls decide "middle pocket" from the index
(p >= 4, cue_physics.c:1097, :1179) rather than from pocket_mid[], which the
header introduces precisely because that index assumption fails on an L-shaped bed
(cue_physics.h:343–348).cue_ai.c)Summarised for completeness; the full analysis is available separately.
cue_ai.c:2735–2767) running one
simulation per frame so the render loop stays live. Stage 1 scores every (target,
pocket) pair analytically (cap 96); stage 2 selects one pair; stage 3 builds variants
of that pair only (cue_ai.c:5181–5195, :5406–5416).cue_ai.c:5438–5442). Power (14 levels), vertical spin (7 levels) and
side are swept; side is compiled to a single level 0.0 by default, on measurements: "Over
60 self-play frames on the 12 ft table, two seeds, turning it on took the pot rate from
79% to 64% and the mean best break from 30 to 18" (cue_ai.c:1810–1812). Elevation is
derived geometrically, not searched (cue_ai.c:490–520).ai_sim() (cue_ai.c:527–704) clones into static scratch, sets
cue_phys_set_substep and cue_phys_set_fast_pot(1), strikes, and steps at dt = 0.05
up to 220 times. The substep now defaults to the live 1/2000 s
(cue_ai.c:185–188) after a measurement: matching the live step "took the mean best
break from 30.0 to 32.6 over 60 frames on two seeds, cut 'potted, then nothing on' from
21.9% to 20.3%" (cue_ai.c:176–184). Inline comments at cue_ai.c:542 and :2680 still
describe a coarser step and are stale relative to that default.err * gear, two passes (cue_ai.c:2782–2840). The sign
matters: "each pass doubles the error instead of removing it. Measured, it took the pot
rate from 87% to 31%" (cue_ai.c:3255–3257). Squirt is cancelled exactly in the break
generator using the live cue_phys_squirt() (cue_ai.c:3560).cue_phys_set_substep and cue_phys_set_fast_pot are global engine
state set and restored around each sim, so the search is not reentrant with live
stepping; the scratch world is a single static buffer.cue_ai.c:3294–3297). Skill also shapes the search budget
("~10 for a rookie up to SIM_CAP for The Machine", cue_ai.c:5850–5871).The cushion is integrated over its own impulse, with two simultaneous contacts. Almost every billiards simulator resolves a rail as one instantaneous impulse against a tilted wall. This one implements Mathavan, Jackson & Parkin's scheme — 96 impulse slices, the slip direction free to rotate during the contact, energetic (Stronge) termination at — and, crucially, carries the cloth contact simultaneously with the cushion contact, which is what makes the ball's spin evolve correctly through the impact. The code cites Biber, Champneys & Szalai for why the single-impulse class cannot be fixed by tuning. On top of that, restitution is speed-dependent and per-table rubber (, floored), the contact height is recomputed per impact from the ball's actual altitude (so an airborne ball meets the nose below its centre and climbs), and the cloth's share of the impulse going negative is the physical trigger for the ball leaving the bed rather than a special case.
The pocket is a signed distance field, and the same field is what you see.
cue_phys_cut_out() gives the exact distance to an arc-with-two-tangent-legs cloth cut
in closed form, with the piece selected by the signs of two local coordinates and no
search. Its numerical gradient is the surface normal for the slate face; the same
function decides where the cloth ends for the renderer, where the lip holds a ball, and
whether a ball with no cloth under it is potted. Three concentric circles are kept
deliberately distinct and deliberately reconciled — the drop/bore circle (capture, the
hole in the floor, the jaw curve's terminus, the cushion-clipping circle), the cut circle
(where slate ends), and the lip roll — and several of the file's most detailed comments
are about failures caused by two of them drifting apart by millimetres. The lip itself
is a genuine constraint with a leave-the-surface criterion
and rolling-without-slipping acceleration down it, so a firm
pot flies off the crest and a dying one is carried round — behaviour that falls out of
the model rather than being scripted.
The strike is measured off a 500 Hz pose ring using the runtime's own fused
velocity, not off differenced positions. The team measured, before building it, that
the OpenXR runtime returns genuinely new poses at ~480 Hz (0.4% collinear at 2 ms, where
interpolation would give six in seven), and then measured that differencing positions
destroys exactly the peak a cue delivery consists of ("a player swinging as hard as they
could produced 12 mm of tip travel in 2.1 ms however hard they swung, and two very
different strokes both scored 5.7 m/s"). So the ring chains XrSpaceVelocity onto each
xrLocateSpace, the tip's velocity is from
the fusion, and the strike walks the tip's real 2 ms-resolved path as a sequence of
segment–sphere tests — recovering the cue's axis at the sub-frame instant of contact,
not just its path. The two cue modes reconstruct different things (rigid rotation of an
offset vs a tip sliding on a locked line) because reconstructing a two-handed cue as a
rigid body "would invent up to 1.5 m × ω of tip travel out of a turn of the wrist". A
frame-rate strike test would step over a 52 mm ball at 8 m/s.
Bit-level determinism is a first-class design constraint, and it visibly shaped the
numerics. Because online play is lockstep on six numbers rather than on ball positions,
the engine may not call the platform's libm: cue_trig.h is a from-scratch sin/cos using
only exactly-rounded operations, the cushion solver's atan2/cos/sin were removed
algebraically (576 libm calls per rail contact eliminated by recognising
), the cushion tilt is stored as a sine/cosine pair
rather than an angle, and the per-shot contact log is a fixed array so nothing allocates.
The sensitivity that forced this is quantified: on a UK 8-ball break, a 100 nm
perturbation of the cue ball moves the worst ball 0.001 mm at 2 m/s and 0.372 mm at
12 m/s — "a tenth of a millimetre in comes out a quarter of a METRE", reported from play
as two headsets disagreeing about whether a ball went down.
An honourable mention: the rigid-body solver's sphere-only "level support" sleep rule — a ball may sleep only when the exponentially-averaged total contact impulse (normal and friction) opposes gravity to within a quarter of a degree, a threshold derived from rather than tuned — is a considerably more careful sleep criterion than a general-purpose engine usually carries, and exists because a ball at rest on a three-quarter-degree ball-return gully is otherwise indistinguishable from one coasting through zero on it.