diff --git a/ALGORITHMS.md b/ALGORITHMS.md index 3a3dabf..627a4c2 100644 --- a/ALGORITHMS.md +++ b/ALGORITHMS.md @@ -183,14 +183,18 @@ The center is the average of the points, weighted by their separation (as Box3D: twice). µ is the static friction when the center slides slower than 1 cm/s, the dynamic friction otherwise. ### Restitution -Applied once after the substeps, for the contacts hitting faster than 1 m/s. The bounce impulse goes towards the -velocity `-e * vn_before` (Newton), and is at most `e` times the impulse which stopped the point, its normal impulse of -the step (Poisson's hypothesis, W. J. Stronge, Impact Mechanics): +Applied once after the substeps, for the contacts hitting faster than 1 m/s, once their compression is over (the +point doesn't approach anymore). The bounce impulse goes towards the velocity `-e * vn_impact` (Newton), and is at most +`e` times the impulse of the compression (Poisson's hypothesis, W. J. Stronge, Impact Mechanics): ```` -λ = max(0, min(-m (vn + e * vn_before), e * λ_step)) +λ = max(0, min(-m (vn + e * vn_impact), e * λ_compression)) ```` +An impact starting at the very end of a step is compressed over 2 steps: the point keeps its approach velocity and its +compression impulse (`ImpactVelocity`, `CompressionImpulse`, warm started like the impulses) and bounces at the end of +the second step, with the whole impulse. Bounced at the end of the first step, a ball dropped from 1 m at 60 Hz gave +back 10 % of its height instead of 92 % (`TestBounceRestitution`, at every rate). Both are needed: a pile of balls bouncing with `e = 1` gains energy with Newton alone (411 J) or Poisson alone -(3523 J), not with both (`TestRestitutionNeverAddsEnergy`). The bounce uses the velocity before the step: a body not +(3523 J), not with both (`TestRestitutionNeverAddsEnergy`). The bounce uses the approach velocity of the impact: a body not round and spinning fast can turn its point away before the end of the step, and bounce higher than it fell over `e = 0.5` (see ARCHITECTURE.md, as documented by Jolt). Newton's law is the one of the game engines (Box2D, Jolt). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6637859..0a7419f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -113,9 +113,9 @@ restitution, continuous collision, islands), without allocation. of contact never stops (no sleep). - A capsule resting across a bump of a terrain can stay a few mm in the terrain: the contact of a triangle comes from the feature of the body above the triangle, the middle of the capsule is missed. -- The restitution is applied once per step, with the velocity before the step: a body not round (box, capsule), - bouncy (`e` over 0.5) and spinning fast (20-50 rad/s) can bounce higher than it fell. Measured: up to +60 % of - energy at `e = 1`, never up to `e = 0.5`. Jolt documents the same limit. +- The restitution is applied once per step, with the approach velocity of the impact: a body not round (box, + capsule), bouncy (`e` over 0.5) and spinning fast (10-20 rad/s) can bounce higher than it fell. Measured at 60 Hz + with 8 substeps: up to +21 % of energy at `e = 1`, never up to `e = 0.5`. Jolt documents the same limit. - No kinematic bodies (moving platforms): a body is static or dynamic. - The continuous collision stops the fast bodies against the static bodies (and the bullets against all the bodies), not the other pairs: 2 fast dynamic bodies rely on their speculative contacts (2 cm) and on the spring of the contact. diff --git a/PHYSICS_GUIDE.md b/PHYSICS_GUIDE.md index f6013f8..6760c60 100644 --- a/PHYSICS_GUIDE.md +++ b/PHYSICS_GUIDE.md @@ -128,7 +128,7 @@ are twice as stiff. - Higher values = less overlap under load (stacks), but it is capped at 1/8 of the substeps rate: `substeps / dt / 8`. - Lower values = softer contacts. -With 12 substeps at 50 Hz, a stack of 10 boxes of 50 cm sinks by ~23 mm (Box2D v3.1: 30 mm): a contact sinks by +With 8 substeps at 60 Hz, a stack of 10 boxes of 50 cm sinks by ~32 mm (Box2D v3.1: 30 mm): a contact sinks by (load / mass) g / (2π hertz)² under its load. A stiffer world sinks less, but a heavy body landing on a light one bounces more. @@ -138,7 +138,7 @@ travel during the step. Between 2 dynamic bodies, from 2 cm only: a fast body ca spring of the contact pushes it out. A fast body is also moved back to its first impact with a static body (continuous collision). Set `IsBullet` on a small fast body (a projectile) to stop it on the dynamic bodies too. -A ball at 40 m/s does not go through a 4 cm wall at 50 Hz. +A ball at 40 m/s does not go through a 4 cm wall at 60 Hz (nor at 80 m/s). ### Sleep The bodies touching each other form an island. An island resting for 0.5 s (all its bodies under 0.05 m/s and 0.05 rad/s) diff --git a/README.md b/README.md index 57d4179..edcbd48 100644 --- a/README.md +++ b/README.md @@ -74,18 +74,20 @@ end - The bodies touching each other sleep and wake up together (islands). ### Why not XPBD anymore -Up to v0.2.0, Feather used a simplified XPBD solver. The same scenes (`bench/`, 50 Hz, 12 substeps): +Up to v0.2.0, Feather used a simplified XPBD solver. The same scenes (`bench/`), each version at its own setting: +TGS Soft at 60 Hz with 8 substeps (the setting of Feather for the games), v0.2.0 at 50 Hz with 12 substeps (the +setting AkmonEngine ran it with; v0.2.0 has no default): | Scene | v0.2.0 (XPBD) | TGS Soft | Expected | |---|---|---|---| -| Pyramid of 55 boxes, 3 s | explodes (top box at 134 m) | stands (4.748 m) | 4.750 m | +| Pyramid of 55 boxes, 3 s | explodes (top box at 93 m) | stands (4.744 m) | 4.750 m | | Box on a 20° slope, µ = 0.6 | slides 9.9 m | 0 m | 0 m | -| Box on a 35° slope, µ = 0.3 | slides 16.9 m | 9.654 m | 9.648 m | -| Bounce from 1 m, restitution 0.5 | 0.06 m | 0.24 m | 0.25 m | +| Box on a 35° slope, µ = 0.3 | slides 16.9 m | 9.657 m | 9.648 m | +| Bounce from 1 m, restitution 0.5 | 0.06 m | 0.23 m | 0.25 m | | 10 N during 1 s on 32.7 kg | 15279 m/s | 0.306 m/s | 0.306 m/s | -| Same scene, run twice | 39/40 bodies differ | identical | identical | +| Same scene, run twice | 38/40 bodies differ | identical | identical | | EPA sphere-box normal (p99) | 2.7° | 0.03° | 0° | -| Step, 10 / 100 / 500 bodies resting on the ground (one layer of boxes & spheres), 1 worker | 0.41 / 1.94 / 8.8 ms | 0.03 / 0.27 / 1.28 ms | | +| Step, 10 / 100 / 500 bodies resting on the ground (one layer of boxes & spheres), 1 worker | 0.39 / 1.82 / 8.3 ms | 0.017 / 0.11 / 0.57 ms | | ``` cd bench @@ -93,8 +95,8 @@ go run . # current version go run -tags v020 -modfile=go.v020.mod . # v0.2.0 ``` -Both versions run 12 substeps at 50 Hz. A heavier scene, 500 boxes & spheres falling on each other (`BenchmarkWorldStep`), -takes ~5.5 ms per step on 1 worker, ~1.9 ms on 8 workers. +A heavier scene, 500 boxes & spheres falling on each other (`BenchmarkWorldStep`, 60 Hz, 8 substeps), takes ~2.1 ms +per step on 1 worker, ~0.8 ms on 8 workers; 2000 bodies awake, 6.5 ms and 2 ms. ### Constraints - Contact: generated when a collision is detected between two rigid bodies, up to 4 points (manifold), with friction, diff --git a/bench/baseline.json b/bench/baseline.json index 6ad530e..a66d839 100644 --- a/bench/baseline.json +++ b/bench/baseline.json @@ -3,240 +3,240 @@ "machine": "AMD Ryzen 7 5800X 8-Core Processor, 16 CPUs", "scenes": { "joint chain": { - "fingerprint": "d1a5ea3300908477", + "fingerprint": "bbc058aa78169d6b", "quality": { "energy gain": { "value": 0, "unit": "%" }, "worst stretch": { - "value": 14.543678411172328, + "value": 23.533696297483242, "unit": "mm" } }, - "stepMs": 0.328752724, + "stepMs": 0.22311584999999998, "phasesMs": { - "broad phase": 0.020677631999999998, - "continuous": 0.0071531039999999995, - "islands": 0.00039, - "narrow phase": 0.002524448, - "prepare": 0.007260608, - "restitution": 0.002520576, - "substeps": 0.288045396 + "broad phase": 0.017644506666666667, + "continuous": 0.00651629, + "islands": 0.0003651733333333333, + "narrow phase": 0.001953446666666667, + "prepare": 0.006912803333333333, + "restitution": 0.0024563633333333336, + "substeps": 0.18710773 } }, "pile of 500": { - "fingerprint": "d77ecceae0f7eea3", + "fingerprint": "89a8e3b082e60588", "quality": { "landing depth": { - "value": 0.6667135029494731, + "value": 0.7094191378169967, "unit": "mm" } }, - "stepMs": 0.8717517, + "stepMs": 0.6791402333333334, "phasesMs": { - "broad phase": 0.10925642666666666, - "continuous": 0.006046853333333333, - "islands": 0.0031442933333333334, - "narrow phase": 0.19552514666666668, - "prepare": 0.06466241333333334, - "restitution": 0.022133499999999997, - "substeps": 0.4699285266666666 + "broad phase": 0.08666362222222222, + "continuous": 0.005989455555555555, + "islands": 0.002344294444444444, + "narrow phase": 0.1715676777777778, + "prepare": 0.06523973333333334, + "restitution": 0.022554666666666667, + "substeps": 0.3238681666666667 } }, "pyramid": { - "fingerprint": "8e5af8ae9965f091", + "fingerprint": "54ab4d46f955f41a", "quality": { "worst drift": { - "value": 15.06227698815896, + "value": 15.192243559737292, "unit": "mm" } }, - "stepMs": 0.07744509399999999, + "stepMs": 0.06185963, "phasesMs": { - "broad phase": 0.004263816, - "continuous": 0.000223382, - "islands": 0.000181226, - "narrow phase": 0.01077844, - "prepare": 0.005719314, - "restitution": 0.001080206, - "substeps": 0.05503125 + "broad phase": 0.004302788333333333, + "continuous": 0.000238935, + "islands": 0.00017517166666666668, + "narrow phase": 0.010100315, + "prepare": 0.005921648333333334, + "restitution": 0.0010952066666666668, + "substeps": 0.039858965 } }, "rain on terrain": { - "fingerprint": "1b2a75beae934de5", + "fingerprint": "ce4a4303727cef5d", "quality": { "fell through": { "value": 0, "unit": "" }, "landing depth": { - "value": 9.743546541685353, + "value": 13.293387340110788, "unit": "mm" } }, - "stepMs": 11.176997512, + "stepMs": 8.771267506666666, "phasesMs": { - "broad phase": 0.11957176800000001, - "continuous": 4.397838452, - "islands": 0.0061450639999999996, - "narrow phase": 3.8915379320000003, - "prepare": 0.314780984, - "restitution": 0.046029851999999996, - "substeps": 2.398003156 + "broad phase": 0.11279914666666668, + "continuous": 3.3886523299999998, + "islands": 0.006130206666666666, + "narrow phase": 3.35082459, + "prepare": 0.29880713, + "restitution": 0.04598874, + "substeps": 1.5650291333333333 } }, "slope pile": { - "fingerprint": "e807a7a4a0748f09", + "fingerprint": "75bf00ecbdccec15", "quality": { "landing depth": { - "value": 1.517463711187439, + "value": 1.9723109923968085, "unit": "mm" }, "resting depth": { - "value": 0.22507290545181746, + "value": 0.13806021507134503, "unit": "mm" } }, - "stepMs": 0.7030997449999999, + "stepMs": 0.5377869291666666, "phasesMs": { - "broad phase": 0.084426155, - "continuous": 0.030294555, - "islands": 0.00084816, - "narrow phase": 0.07793892, - "prepare": 0.03624791, - "restitution": 0.010420575000000001, - "substeps": 0.46253020999999994 + "broad phase": 0.07539907916666667, + "continuous": 0.0285632375, + "islands": 0.0009086416666666666, + "narrow phase": 0.07304387083333334, + "prepare": 0.036801824999999996, + "restitution": 0.0106496, + "substeps": 0.31201233749999996 } }, "solver2d ball and chain": { - "fingerprint": "5495df4bcca4c3ab", + "fingerprint": "c8a1bfe7890359b0", "quality": { "finite": { "value": 1, "unit": "" }, "worst gap": { - "value": 0.5545802126429944, + "value": 0.5372876825478579, "unit": "mm" } }, - "stepMs": 0.332302912, + "stepMs": 0.3265644366666667, "phasesMs": { - "broad phase": 0.022754055999999998, - "continuous": 0.00524216, - "islands": 0.000386164, - "narrow phase": 0.001125564, - "prepare": 0.008728556, - "restitution": 0.00268974, - "substeps": 0.291197784 + "broad phase": 0.020490386666666666, + "continuous": 0.004699053333333333, + "islands": 0.00037907333333333333, + "narrow phase": 0.0009667766666666667, + "prepare": 0.008522596666666667, + "restitution": 0.0026978566666666665, + "substeps": 0.28863832333333334 } }, "solver2d bridge": { - "fingerprint": "dd9b5224748922e2", + "fingerprint": "a3db5d8b1a677b65", "quality": { "finite": { "value": 1, "unit": "" }, "sag": { - "value": 0.25237870005618745, + "value": 0.25784495328753465, "unit": "m" }, "worst gap": { - "value": 0.23602681912618909, + "value": 0.2313907905724356, "unit": "mm" } }, - "stepMs": 0.5801432160000001, + "stepMs": 0.5862262166666666, "phasesMs": { - "broad phase": 0.004460856, - "continuous": 0.001942376, - "islands": 0.000806564, - "narrow phase": 0.001015528, - "prepare": 0.015942788, - "restitution": 0.009394512, - "substeps": 0.546367072 + "broad phase": 0.0041852366666666665, + "continuous": 0.0019353299999999999, + "islands": 0.00080361, + "narrow phase": 0.00105747, + "prepare": 0.016008293333333333, + "restitution": 0.009457596666666667, + "substeps": 0.5525685133333333 } }, "solver2d card house": { - "fingerprint": "4fbd38eef3730b31", + "fingerprint": "ea7d1528ea6e2a5e", "quality": { "levels": { "value": 3, "unit": "" }, "worst drift": { - "value": 0.05576265490251589, + "value": 0.14162818999455504, "unit": "mm" } }, - "stepMs": 0.08392965714285715, + "stepMs": 0.08783918095238095, "phasesMs": { - "broad phase": 0.0016234914285714287, - "continuous": 0.0006242857142857143, - "islands": 0.00016634285714285714, - "narrow phase": 0.02821218285714286, - "prepare": 0.005434262857142857, - "restitution": 0.0008903542857142858, - "substeps": 0.046826508571428574 + "broad phase": 0.0016421619047619048, + "continuous": 0.0006579095238095239, + "islands": 0.0001777666666666667, + "narrow phase": 0.027161838095238092, + "prepare": 0.005864728571428571, + "restitution": 0.000954247619047619, + "substeps": 0.051219719047619044 } }, "solver2d centered impact": { - "fingerprint": "ccfa947ac592a6a4", + "fingerprint": "e1bedba8c7512ef8", "quality": { "bounce": { - "value": 1.7398077180601144, + "value": 1.7995910348100592, "unit": "m/s" }, "lateral drift": { - "value": 0.0007475795228636179, + "value": 0.0004816933903688758, "unit": "mm" } }, - "stepMs": 0.006930726666666667, + "stepMs": 0.0070184, "phasesMs": { - "broad phase": 0.00024186666666666667, - "continuous": 0.00010059999999999999, - "islands": 0.00007826666666666667, - "narrow phase": 0.0008808133333333333, - "prepare": 0.00075174, - "restitution": 0.000316, - "substeps": 0.00441984 + "broad phase": 0.00024150000000000002, + "continuous": 0.00010083333333333334, + "islands": 0.00007822222222222223, + "narrow phase": 0.00097985, + "prepare": 0.0007626166666666667, + "restitution": 0.0003129444444444444, + "substeps": 0.0044241 } }, "solver2d circle stack": { - "fingerprint": "0a3f1b475a777524", + "fingerprint": "1a740d7c89ea63b8", "quality": { "horizontal drift": { "value": 0, "unit": "mm" }, "top height error": { - "value": 7.103344548649915, + "value": 7.099453084640217, "unit": "mm" } }, - "stepMs": 0.004133155, + "stepMs": 0.004386412500000001, "phasesMs": { - "broad phase": 0.0007682750000000001, - "continuous": 0.0001083275, - "islands": 0.00007675000000000001, - "narrow phase": 0.000311125, - "prepare": 0.0005085325000000001, - "restitution": 0.00018435, - "substeps": 0.00205999 + "broad phase": 0.0007420916666666667, + "continuous": 0.00011364583333333334, + "islands": 0.00007867291666666667, + "narrow phase": 0.00036327083333333334, + "prepare": 0.000583125, + "restitution": 0.00019571458333333334, + "substeps": 0.002192225 } }, "solver2d confined": { - "fingerprint": "421bb7b06f13eb23", + "fingerprint": "5349e31e6f75876e", "quality": { "escaped": { "value": 0, "unit": "" }, "max speed": { - "value": 5.695985977206491, + "value": 5.571465777301871, "unit": "m/s" }, "row": { @@ -244,45 +244,45 @@ "unit": "" } }, - "stepMs": 0.482003888, + "stepMs": 0.4438929433333333, "phasesMs": { - "broad phase": 0.04847136, - "continuous": 0.00294756, - "islands": 0.001493372, - "narrow phase": 0.103083644, - "prepare": 0.074621552, - "restitution": 0.008087508, - "substeps": 0.24247100800000002 + "broad phase": 0.045836836666666665, + "continuous": 0.002721836666666667, + "islands": 0.0014226100000000002, + "narrow phase": 0.09266798333333334, + "prepare": 0.06887237, + "restitution": 0.00757283, + "substeps": 0.22402076333333332 } }, "solver2d double domino": { - "fingerprint": "130e3ab1df7a73eb", + "fingerprint": "00cfdb3e173939da", "quality": { "fallen": { "value": 15, "unit": "" }, "time of the last": { - "value": 6.016666666666651, + "value": 7.349999999999979, "unit": "s" } }, - "stepMs": 0.11528807875, + "stepMs": 0.11550660104166667, "phasesMs": { - "broad phase": 0.00183274875, - "continuous": 0.00070396875, - "islands": 0.00020990124999999997, - "narrow phase": 0.0206642175, - "prepare": 0.00883972875, - "restitution": 0.0019020525, - "substeps": 0.08095218125 + "broad phase": 0.0018144020833333334, + "continuous": 0.0004748583333333333, + "islands": 0.000212453125, + "narrow phase": 0.02064351979166667, + "prepare": 0.009059655208333333, + "restitution": 0.0019143895833333333, + "substeps": 0.08119891145833333 } }, "solver2d far chain": { - "fingerprint": "7569d5ae36a9d526", + "fingerprint": "920bc3620f9389f9", "quality": { "far deviation": { - "value": 1.0331229789970546e-7, + "value": 1.0814722984127874e-7, "unit": "mm" }, "finite": { @@ -290,26 +290,26 @@ "unit": "" }, "worst gap": { - "value": 25.937047921567068, + "value": 20.40608155134376, "unit": "mm" } }, - "stepMs": 0.6462906420000001, + "stepMs": 0.6553974216666667, "phasesMs": { - "broad phase": 0.051685273999999996, - "continuous": 0.017276807999999998, - "islands": 0.000647306, - "narrow phase": 0.010573994000000001, - "prepare": 0.014943628, - "restitution": 0.004993612, - "substeps": 0.545955958 + "broad phase": 0.047766336666666666, + "continuous": 0.017236493333333335, + "islands": 0.0006970199999999999, + "narrow phase": 0.009058186666666666, + "prepare": 0.015384613333333332, + "restitution": 0.0051592700000000005, + "substeps": 0.5598622316666667 } }, "solver2d far pyramid": { - "fingerprint": "9a28b30be3a8991b", + "fingerprint": "27a026a05e4bed38", "quality": { "far deviation": { - "value": 8.907780632216337, + "value": 0.6805923984898046, "unit": "mm" }, "worst drift": { @@ -317,45 +317,45 @@ "unit": "mm" } }, - "stepMs": 2.8456944539999998, + "stepMs": 3.416486503333333, "phasesMs": { - "broad phase": 0.087139968, - "continuous": 0.00620242, - "islands": 0.003377798, - "narrow phase": 0.5585856760000001, - "prepare": 0.22202198, - "restitution": 0.037133066, - "substeps": 1.9297350340000001 + "broad phase": 0.08273485166666666, + "continuous": 0.007173518333333333, + "islands": 0.0038471699999999996, + "narrow phase": 0.6604326416666667, + "prepare": 0.2593133266666667, + "restitution": 0.042551313333333333, + "substeps": 2.3586961066666667 } }, "solver2d far recovery": { - "fingerprint": "998f0b0d592fb07d", + "fingerprint": "5d76653bd827d421", "quality": { "far deviation": { - "value": 0.00036292906023460486, + "value": 0.00027029356816151974, "unit": "mm" }, "max speed": { - "value": 3.5716880471398262, + "value": 3.5794401735540036, "unit": "m/s" } }, - "stepMs": 0.5423482933333333, + "stepMs": 0.5060772611111111, "phasesMs": { - "broad phase": 0.006252366666666667, - "continuous": 0.0008281133333333333, - "islands": 0.0005372066666666667, - "narrow phase": 0.16870949666666665, - "prepare": 0.03308945666666667, - "restitution": 0.004388276666666667, - "substeps": 0.32827197333333336 + "broad phase": 0.006102769444444445, + "continuous": 0.0007853972222222222, + "islands": 0.0005214444444444444, + "narrow phase": 0.16014925833333335, + "prepare": 0.030216883333333333, + "restitution": 0.004052822222222223, + "substeps": 0.30395435 } }, "solver2d far stack": { - "fingerprint": "9bdfc2ae4956f529", + "fingerprint": "11ea1fce583a161e", "quality": { "far deviation": { - "value": 0.11290130794691286, + "value": 0.8869082560041711, "unit": "mm" }, "worst drift": { @@ -363,45 +363,45 @@ "unit": "mm" } }, - "stepMs": 0.018295106000000002, + "stepMs": 0.016550343333333332, "phasesMs": { - "broad phase": 0.000473244, - "continuous": 0.00014394, - "islands": 0.00010472, - "narrow phase": 0.003934344, - "prepare": 0.0016116939999999999, - "restitution": 0.000529246, - "substeps": 0.011373557999999999 + "broad phase": 0.000469625, + "continuous": 0.00013216833333333332, + "islands": 0.00009938333333333334, + "narrow phase": 0.0034522449999999996, + "prepare": 0.0015034016666666667, + "restitution": 0.0004838066666666666, + "substeps": 0.010284610000000001 } }, "solver2d friction ramp": { - "fingerprint": "e49861060e0a7f62", + "fingerprint": "2f29997fb3f6f207", "quality": { "sliding error": { - "value": 6.0675788535160535, + "value": 5.056316187546095, "unit": "mm" }, "stopped slide": { - "value": 0, + "value": 2.446734066676593e-13, "unit": "mm" } }, - "stepMs": 0.028031592, + "stepMs": 0.0278904, "phasesMs": { - "broad phase": 0.001023616, - "continuous": 0.00017544000000000001, - "islands": 0.00010688, - "narrow phase": 0.011025952, - "prepare": 0.002257776, - "restitution": 0.00071656, - "substeps": 0.012594568 + "broad phase": 0.0008240733333333333, + "continuous": 0.0001772, + "islands": 0.0001004, + "narrow phase": 0.01078358, + "prepare": 0.0023112266666666667, + "restitution": 0.0007275333333333334, + "substeps": 0.012834586666666667 } }, "solver2d high mass ratio 1": { - "fingerprint": "e7874f6ac25fd537", + "fingerprint": "2556598fc959e80d", "quality": { "heavy cube sag": { - "value": 127.99081789071032, + "value": 127.56313513180118, "unit": "mm" }, "layers": { @@ -409,115 +409,115 @@ "unit": "" }, "worst drift": { - "value": 105.79202475015144, + "value": 88.69691065809597, "unit": "mm" } }, - "stepMs": 1.698726344, + "stepMs": 1.7505143300000001, "phasesMs": { - "broad phase": 0.01593834, - "continuous": 0.002268616, - "islands": 0.00188574, - "narrow phase": 0.562861172, - "prepare": 0.11869384399999999, - "restitution": 0.014986652, - "substeps": 0.981310976 + "broad phase": 0.01380236, + "continuous": 0.00239012, + "islands": 0.0017529133333333332, + "narrow phase": 0.4902900233333333, + "prepare": 0.12939972333333333, + "restitution": 0.01589753, + "substeps": 1.0962155533333333 } }, "solver2d high mass ratio 2": { - "fingerprint": "b0c580b92f03cacf", + "fingerprint": "35f80b29687cde2f", "quality": { "bounce": { - "value": 2.2252620543727213, + "value": 2.0355465723505186, "unit": "m/s" }, "slab sag": { - "value": 67.77438017502391, + "value": 67.63919875990432, "unit": "mm" }, "small cubes drift": { - "value": 21.56214604243897, + "value": 16.468452377556567, "unit": "mm" }, "small cubes into ground": { - "value": 577.2579911571269, + "value": 530.0366269782514, "unit": "mm" } }, - "stepMs": 0.017935904, + "stepMs": 0.015759786666666664, "phasesMs": { - "broad phase": 0.00041252, - "continuous": 0.00019628, - "islands": 0.0000992, - "narrow phase": 0.005752452, - "prepare": 0.001354568, - "restitution": 0.000573644, - "substeps": 0.009424 + "broad phase": 0.0003818066666666667, + "continuous": 0.00015863333333333333, + "islands": 0.00009056666666666667, + "narrow phase": 0.004262806666666666, + "prepare": 0.0012759066666666666, + "restitution": 0.0005415, + "substeps": 0.0089258 } }, "solver2d high mass ratio 3": { - "fingerprint": "162be214ceca1077", + "fingerprint": "16fc3d89762c7f2b", "quality": { "bounce": { - "value": 2.2253312335268416, + "value": 2.035546593114035, "unit": "m/s" }, "slab sag": { - "value": 67.77361632275891, + "value": 67.63920297683335, "unit": "mm" }, "small cubes drift": { - "value": 21.520070758926067, + "value": 16.468453893118642, "unit": "mm" }, "small cubes into ground": { - "value": 577.2579914400629, + "value": 530.0366268830902, "unit": "mm" } }, - "stepMs": 0.018962556, + "stepMs": 0.016582363333333332, "phasesMs": { - "broad phase": 0.00039088, - "continuous": 0.00017580000000000002, - "islands": 0.00009392399999999999, - "narrow phase": 0.006965624, - "prepare": 0.001326616, - "restitution": 0.0005552399999999999, - "substeps": 0.009322272 + "broad phase": 0.00037723333333333336, + "continuous": 0.00014847, + "islands": 0.00008957, + "narrow phase": 0.005100736666666667, + "prepare": 0.00129302, + "restitution": 0.0005405433333333334, + "substeps": 0.008910086666666667 } }, "solver2d joint grid": { - "fingerprint": "3bf73d7661991d94", + "fingerprint": "fc6397aa108a1b61", "quality": { "finite": { "value": 1, "unit": "" }, "max speed": { - "value": 30.115023171399358, + "value": 29.677462010412945, "unit": "m/s" }, "worst gap": { - "value": 88.35962353634609, + "value": 71.45402387483796, "unit": "mm" } }, - "stepMs": 4.44351414, + "stepMs": 4.297956633333333, "phasesMs": { - "broad phase": 0.8097767066666667, - "continuous": 0.06023162000000001, - "islands": 0.00831516, - "narrow phase": 0.2033266, - "prepare": 0.25451162, - "restitution": 0.0297918, - "substeps": 3.07573754 + "broad phase": 0.7044892944444444, + "continuous": 0.04431798888888889, + "islands": 0.008377738888888889, + "narrow phase": 0.18656302222222224, + "prepare": 0.2509896555555555, + "restitution": 0.030165805555555557, + "substeps": 3.071167677777778 } }, "solver2d overlap recovery": { - "fingerprint": "5bbb7588f8e57051", + "fingerprint": "d0e49c5c8f23fdb8", "quality": { "final overlap": { - "value": 3.321096845759746, + "value": 3.5904768847053634, "unit": "mm" }, "layers": { @@ -525,96 +525,96 @@ "unit": "" }, "max speed": { - "value": 3.5716880409799106, + "value": 3.579440180143181, "unit": "m/s" } }, - "stepMs": 0.31934376400000003, + "stepMs": 0.30256958666666667, "phasesMs": { - "broad phase": 0.004773896, - "continuous": 0.000481972, - "islands": 0.00036328, - "narrow phase": 0.101049792, - "prepare": 0.019754956, - "restitution": 0.002655388, - "substeps": 0.19003628 + "broad phase": 0.004613256666666667, + "continuous": 0.00045770666666666664, + "islands": 0.0003148033333333333, + "narrow phase": 0.09700434333333334, + "prepare": 0.01850401, + "restitution": 0.00243612, + "substeps": 0.17902811333333332 } }, "solver2d pyramid": { - "fingerprint": "6966ec7612fd5700", + "fingerprint": "040fe6c881f7d11d", "quality": { "layers": { "value": 8, "unit": "" }, "worst drift": { - "value": 1.45745153162969, + "value": 1.3984703091777932, "unit": "mm" } }, - "stepMs": 1.170504028, + "stepMs": 1.1881094466666666, "phasesMs": { - "broad phase": 0.024777076000000002, - "continuous": 0.001236692, - "islands": 0.0012278880000000001, - "narrow phase": 0.371713392, - "prepare": 0.090282104, - "restitution": 0.008843400000000001, - "substeps": 0.6716893119999999 + "broad phase": 0.025294006666666667, + "continuous": 0.00136154, + "islands": 0.0012782466666666666, + "narrow phase": 0.32590651, + "prepare": 0.09391947333333334, + "restitution": 0.009792363333333333, + "substeps": 0.72968929 } }, "solver2d rush": { - "fingerprint": "13c44583001f28b6", + "fingerprint": "0e2d9d71fb7a52f9", "quality": { "final overlap": { - "value": 2.386494028450481, + "value": 2.417279648855186, "unit": "mm" }, "final speed": { - "value": 0.020914631998823915, + "value": 0.16259664402005986, "unit": "m/s" }, "max speed": { - "value": 12.987043356298601, + "value": 12.944602038140715, "unit": "m/s" } }, - "stepMs": 0.066552784, + "stepMs": 0.06329681, "phasesMs": { - "broad phase": 0.017071128, - "continuous": 0.00398104, - "islands": 0.0008597240000000001, - "narrow phase": 0.00011212, - "prepare": 0.007568648, - "restitution": 0.006720804, - "substeps": 0.03012368 + "broad phase": 0.013700053333333333, + "continuous": 0.0039777599999999995, + "islands": 0.00083827, + "narrow phase": 0.00008236666666666666, + "prepare": 0.00768381, + "restitution": 0.006660816666666667, + "substeps": 0.030232233333333334 } }, "solver2d single box": { - "fingerprint": "7ac48fd94a2b753c", + "fingerprint": "6ff146d7621f506c", "quality": { "height error": { - "value": 0.06902504482564353, + "value": 0.06902504565653445, "unit": "mm" }, "rest drift": { - "value": 0.00010860480059715627, + "value": 0.00010093712410341737, "unit": "mm" } }, - "stepMs": 0.004723846666666667, + "stepMs": 0.002994583333333333, "phasesMs": { - "broad phase": 0.00027573333333333333, - "continuous": 0.00014033333333333332, - "islands": 0.0001222, - "narrow phase": 0.00025140666666666667, - "prepare": 0.0004670066666666667, - "restitution": 0.00026307333333333333, - "substeps": 0.0030406266666666666 + "broad phase": 0.00015972222222222223, + "continuous": 0.000077, + "islands": 0.0000665, + "narrow phase": 0.0001747777777777778, + "prepare": 0.00031716666666666666, + "restitution": 0.00018239444444444444, + "substeps": 0.001903411111111111 } }, "solver2d stretched chain": { - "fingerprint": "f6e92f3665ce4bd4", + "fingerprint": "9f63a900d330ab9e", "quality": { "final gap": { "value": 0.07178605861213327, @@ -625,7 +625,7 @@ "unit": "" }, "max speed": { - "value": 254.50238414789507, + "value": 256.8677345941416, "unit": "m/s" }, "rest gap": { @@ -633,22 +633,22 @@ "unit": "mm" } }, - "stepMs": 0.043703542, + "stepMs": 0.044242175, "phasesMs": { - "broad phase": 0.003121604, - "continuous": 0.00040764, - "islands": 0.00017752000000000002, - "narrow phase": 0.000200042, - "prepare": 0.001950956, - "restitution": 0.00043378, - "substeps": 0.037176274 + "broad phase": 0.0030517966666666666, + "continuous": 0.0003975683333333333, + "islands": 0.000201055, + "narrow phase": 0.00011893499999999999, + "prepare": 0.0019251283333333333, + "restitution": 0.00043979, + "substeps": 0.0378815 } }, "solver2d vertical stack": { - "fingerprint": "51fef4fd3c94bf8e", + "fingerprint": "48a4cc8b0c742495", "quality": { "horizontal drift": { - "value": 2.845671443000555, + "value": 3.01593245638026, "unit": "mm" }, "layers": { @@ -656,61 +656,61 @@ "unit": "" } }, - "stepMs": 0.022261464, + "stepMs": 0.020979173333333333, "phasesMs": { - "broad phase": 0.000911124, - "continuous": 0.0001988, - "islands": 0.00012328, - "narrow phase": 0.0049292, - "prepare": 0.00218402, - "restitution": 0.000839008, - "substeps": 0.012949312000000001 + "broad phase": 0.0009151766666666666, + "continuous": 0.0002032, + "islands": 0.00013230333333333333, + "narrow phase": 0.0037009933333333332, + "prepare": 0.0022184033333333335, + "restitution": 0.00081438, + "substeps": 0.012852416666666667 } }, "solver2d warm start energy": { - "fingerprint": "8dcfb49e8c691bf3", + "fingerprint": "64080265c973a518", "quality": { "overshoot": { "value": 0, "unit": "mm" } }, - "stepMs": 0.0036926679999999996, + "stepMs": 0.0037194666666666666, "phasesMs": { - "broad phase": 0.00024568, - "continuous": 0.000091444, - "islands": 0.00007408, - "narrow phase": 0.00032404, - "prepare": 0.00057076, - "restitution": 0.000163084, - "substeps": 0.002104612 + "broad phase": 0.00024100333333333334, + "continuous": 0.0000922, + "islands": 0.00007443333333333333, + "narrow phase": 0.00032637, + "prepare": 0.0005691, + "restitution": 0.0001652, + "substeps": 0.0021346566666666667 } }, "terrain piles": { - "fingerprint": "4a6b6d3833b795ba", + "fingerprint": "ca16c8f217a79890", "quality": { "median landing depth": { - "value": 7.875508690151446, + "value": 7.26281832420491, "unit": "mm" }, "worst landing depth": { - "value": 17.85679629331417, + "value": 12.82998497579694, "unit": "mm" }, "worst resting depth": { - "value": 7.342632951171865, + "value": 7.345867902704917, "unit": "mm" } }, - "stepMs": 3.997912273, + "stepMs": 3.070904823333333, "phasesMs": { - "broad phase": 0.031085574, - "continuous": 1.954000463, - "islands": 0.0015134764999999999, - "narrow phase": 1.0432447784999999, - "prepare": 0.089018852, - "restitution": 0.013083258, - "substeps": 0.865058801 + "broad phase": 0.02748282375, + "continuous": 1.4693527233333332, + "islands": 0.0014843529166666668, + "narrow phase": 0.9272526575000001, + "prepare": 0.08509789708333333, + "restitution": 0.0127703225, + "substeps": 0.5466390391666667 } } } diff --git a/bench/current.go b/bench/current.go index d7ce7b9..8d89f4a 100644 --- a/bench/current.go +++ b/bench/current.go @@ -38,6 +38,12 @@ func narrow(a, b *actor.RigidBody) (bool, mgl64.Vec3, float64, int) { return true, m.Normal, -m.MinSeparation(), m.Count } +// the setting of Feather for the games: 60 Hz, 8 sub-steps +const ( + dt = 1.0 / 60 + substeps = 8 +) + func world(workers int) *feather.World { return &feather.World{ Gravity: mgl64.Vec3{0, -g, 0}, diff --git a/bench/main.go b/bench/main.go index 1235421..b727a7b 100644 --- a/bench/main.go +++ b/bench/main.go @@ -7,7 +7,8 @@ // go run . -update # write baseline.json, after a wanted change // go run . -scenes ; go run . -compare # the scenes of Solver2D (reference.go) // -// Every scene runs at AkmonEngine's rate: 50 Hz, 12 sub-steps, one worker. +// Every scene runs at the setting of each version, one worker: 60 Hz with 8 sub-steps for the working tree (the +// setting of Feather for the games), 50 Hz with 12 sub-steps for v0.2.0 (the setting AkmonEngine ran it with). package main import ( @@ -24,9 +25,7 @@ import ( ) const ( - dt = 1.0 / 50 - substeps = 12 - g = 9.81 + g = 9.81 ) func body(w *feather.World, t actor.Transform, s actor.ShapeInterface, typ actor.BodyType, mu, e float64) *actor.RigidBody { @@ -182,7 +181,7 @@ func force() { b := body(w, tr(mgl64.Vec3{}, mgl64.QuatIdent()), &actor.Sphere{Radius: 0.25}, actor.BodyTypeDynamic, 0, 0) m := b.Material.GetMass() F := 10.0 - for i := 0; i < 50; i++ { + for i := 0; i < int(math.Round(1/dt)); i++ { b.AddForce(mgl64.Vec3{F, 0, 0}) w.Step(dt) } diff --git a/bench/scenes/adapter_current.go b/bench/scenes/adapter_current.go index 54ee57e..e084ded 100644 --- a/bench/scenes/adapter_current.go +++ b/bench/scenes/adapter_current.go @@ -43,6 +43,13 @@ func moved(b *actor.RigidBody) { b.UpdateAABB() } +// the setting of Feather for the games: 60 Hz (the default rate of Box3D, the reference, which runs its default 4 +// substeps) with 8 substeps +const ( + Dt = 1.0 / 60 + substeps = 8 +) + func newWorld() *feather.World { return &feather.World{ Gravity: mgl64.Vec3{0, -gravity, 0}, diff --git a/bench/scenes/adapter_v020.go b/bench/scenes/adapter_v020.go index 244e2d7..3544645 100644 --- a/bench/scenes/adapter_v020.go +++ b/bench/scenes/adapter_v020.go @@ -39,6 +39,12 @@ func ball(w *feather.World, a, b *actor.RigidBody, anchor mgl64.Vec3) { // moved: v0.2.0 computes the AABB at each step func moved(b *actor.RigidBody) {} +// the setting AkmonEngine ran v0.2.0 with: 50 Hz, 12 substeps (v0.2.0 has no default) +const ( + Dt = 1.0 / 50 + substeps = 12 +) + func newWorld() *feather.World { return &feather.World{ Gravity: mgl64.Vec3{0, -gravity, 0}, diff --git a/bench/scenes/scenes.go b/bench/scenes/scenes.go index f7d835a..98cfbbb 100644 --- a/bench/scenes/scenes.go +++ b/bench/scenes/scenes.go @@ -16,14 +16,10 @@ import ( "github.com/go-gl/mathgl/mgl64" ) -const ( - // Dt of a step (s): the default rate of Box3D, the reference. 8 substeps: the setting of Feather for the games - // (Box3D runs its default, 4). The contacts are springs of 30 Hz whatever the substeps: a light body under a load - // sinks by (mass ratio) g / ω² at rest, and more substeps make the stacks, the impacts and the far scenes better - Dt = 1.0 / 60 - substeps = 8 - gravity = 9.81 -) +// Each version runs at its own setting (adapter_current.go, adapter_v020.go): Dt of a step and the substeps. +// The contacts are springs of 30 Hz whatever the substeps: a light body under a load sinks by (mass ratio) g / ω² at +// rest, and more substeps make the stacks, the impacts and the far scenes better +const gravity = 9.81 // Size of a scene type Size int diff --git a/bench/v020.go b/bench/v020.go index eb1cb4d..86ed3e8 100644 --- a/bench/v020.go +++ b/bench/v020.go @@ -45,6 +45,12 @@ func regressions(update bool) bool { return false } +// the setting AkmonEngine ran v0.2.0 with: 50 Hz, 12 sub-steps (v0.2.0 has no default) +const ( + dt = 1.0 / 50 + substeps = 12 +) + func world(workers int) *feather.World { return &feather.World{ Gravity: mgl64.Vec3{0, -g, 0}, diff --git a/constraint/contact.go b/constraint/contact.go index 3d6a249..2815ae7 100644 --- a/constraint/contact.go +++ b/constraint/contact.go @@ -17,6 +17,12 @@ type ContactPoint struct { // NormalImpulse applied by the solver during the last step (N·s), to warm start the next step NormalImpulse float64 + // Impact in progress, kept from a step to the next: the approach velocity when the point started to hit (m/s, + // 0 without impact) and the normal impulse of its compression so far (N·s). The point bounces once the + // compression is over, which can be during the next step when the impact starts at the very end of a step + ImpactVelocity float64 + CompressionImpulse float64 + // LocalAnchorA is the point on the surface of A, in the local space of A, LocalAnchorB the point on the // surface of B, in the local space of B. They find the same point in the next step (warm starting), // and move the contact with the bodies (pair cache) diff --git a/solver.go b/solver.go index b23da88..e3e702d 100644 --- a/solver.go +++ b/solver.go @@ -152,6 +152,9 @@ type contactPoint struct { normalImpulse float64 totalNormalImpulse float64 // the normal impulse of the step: the impulse which stopped the point, for the restitution normalVelocity float64 // before the solver, for the restitution + // the impact in progress (see ContactPoint): its approach velocity, and the impulse of its compression so far + impactVelocity float64 + compressionImpulse float64 leverArm float64 // distance to the friction center: the twist friction it can hold } @@ -403,8 +406,9 @@ func (s *solver) prepareConstraint(i int) { cp.baseSeparation = point.Separation - cp.coreB.Sub(cp.coreA).Dot(c.normal) cp.normal = makeJacobian(stateA, stateB, cp.rA, cp.rB, c.normal) - // Warm starting: the impulses of the previous step + // Warm starting: the impulses of the previous step, and the impact in progress cp.normalImpulse = point.NormalImpulse + cp.impactVelocity, cp.compressionImpulse = point.ImpactVelocity, point.CompressionImpulse cp.normalVelocity = relativeVelocity(stateA, stateB, cp.rA, cp.rB).Dot(c.normal) } c.prepareFriction(stateA, stateB, staticFriction, dynamicFriction) @@ -1101,10 +1105,13 @@ func clampDisk(impulse *[2]float64, radius float64) { } // ========== RESTITUTION ========== -// restitution, after the substeps: a point which hit faster than RestitutionThreshold bounces. Its impulse goes towards -// the velocity -restitution * its velocity before the step (Newton), and is at most restitution times the impulse which -// stopped it, its normal impulse of the step (Poisson's hypothesis, W. J. Stronge, Impact Mechanics): a pile of bodies -// doesn't give back more than it absorbed +// restitution, after the substeps: a point which hit faster than RestitutionThreshold bounces once its compression is +// over (it doesn't approach anymore). Its impulse goes towards the velocity -restitution * its approach velocity +// (Newton), and is at most restitution times the impulse of the compression (Poisson's hypothesis, W. J. Stronge, +// Impact Mechanics): a pile of bodies doesn't give back more than it absorbed. An impact which starts at the very end of +// a step is compressed over 2 steps: the point keeps its approach velocity and its compression impulse, and bounces at +// the end of the second step, with the whole impulse. Bounced at the end of the first step, it would give back +// restitution times a fraction of the impulse only func (s *solver) restitution() { s.solveConstraints(s.jobs.restitution, nil) } @@ -1116,15 +1123,24 @@ func (s *solver) restitutionConstraint(c *contactConstraint) { stateA, stateB := s.state(c.indexA), s.state(c.indexB) for j := 0; j < c.pointsCount; j++ { cp := &c.points[j] - if cp.normalVelocity >= -RestitutionThreshold || cp.totalNormalImpulse <= 0 { - continue + if cp.impactVelocity == 0 { + if cp.normalVelocity >= -RestitutionThreshold || cp.totalNormalImpulse <= 0 { + continue + } + cp.impactVelocity = cp.normalVelocity } + cp.compressionImpulse += cp.totalNormalImpulse velocity := cp.normal.velocity(stateA, stateB, c.normal) - newton := -cp.normal.mass * (velocity + c.restitution*cp.normalVelocity) - poisson := c.restitution * cp.totalNormalImpulse + if velocity < -actor.DefaultSleepSpeed { + // still approaching: the compression goes on in the next step + continue + } + newton := -cp.normal.mass * (velocity + c.restitution*cp.impactVelocity) + poisson := c.restitution * cp.compressionImpulse if impulse := math.Min(newton, poisson); impulse > 0 { cp.addNormalImpulse(stateA, stateB, c.normal, impulse) } + cp.impactVelocity, cp.compressionImpulse = 0, 0 } } @@ -1140,6 +1156,7 @@ func (s *solver) storeImpulsesConstraint(i int) { point := &c.manifold.Points[j] cp := &c.points[j] point.NormalImpulse = cp.normalImpulse + point.ImpactVelocity, point.CompressionImpulse = cp.impactVelocity, cp.compressionImpulse } c.manifold.FrictionImpulse = c.tangents[0].Mul(c.frictionImpulse[0]).Add(c.tangents[1].Mul(c.frictionImpulse[1])) c.manifold.TwistImpulse = c.twistImpulse diff --git a/world.go b/world.go index f64d284..e71c779 100644 --- a/world.go +++ b/world.go @@ -513,6 +513,7 @@ func warmStartPair(manifolds, previous []constraint.Manifold) { used[closestManifold][closest] = true point := &previous[closestManifold].Points[closest] manifold.Points[j].NormalImpulse = point.NormalImpulse + manifold.Points[j].ImpactVelocity, manifold.Points[j].CompressionImpulse = point.ImpactVelocity, point.CompressionImpulse if source < 0 { source = closestManifold } diff --git a/world_physics_test.go b/world_physics_test.go index 19dd473..94adb2c 100644 --- a/world_physics_test.go +++ b/world_physics_test.go @@ -11,11 +11,10 @@ import ( "github.com/go-gl/mathgl/mgl64" ) -// Physical scenarios with a known answer, at the rate AkmonEngine runs Feather: -// 50 Hz, 12 sub-steps. +// Physical scenarios with a known answer, at the setting of Feather for the games: 60 Hz, 8 sub-steps. const ( - sceneDt = 1.0 / 50 - sceneSubsteps = 12 + sceneDt = 1.0 / 60 + sceneSubsteps = 8 sceneGravity = 9.81 cubeHalf = 0.25 ) @@ -189,13 +188,14 @@ func TestBounceRestitution(t *testing.T) { ground := addGround(w, 0) ground.Material.Restitution = e ball := addBody(w, mgl64.Vec3{0, 1 + cubeHalf, 0}, mgl64.QuatIdent(), &actor.Sphere{Radius: cubeHalf}, actor.BodyTypeDynamic, 0, e) + // the apex of the rebound: the height reached while going up after the hit hit, apex := false, 0.0 simulate(w, 2.5, func() { h := ball.Transform.Position.Y() - cubeHalf if h < 0.01 { hit = true } - if hit { + if hit && ball.Velocity.Y() > 0 { apex = math.Max(apex, h) } }) @@ -217,7 +217,7 @@ func TestForcesAreSI(t *testing.T) { m := ball.Material.GetMass() inertia := ball.InertiaLocal.At(0, 0) const force, torque = 10.0, 3.0 - for i := 0; i < 50; i++ { + for i := 0; i < int(math.Round(1/sceneDt)); i++ { ball.AddForce(mgl64.Vec3{force, 0, 0}) ball.AddTorque(mgl64.Vec3{0, torque, 0}) w.Step(sceneDt) @@ -250,8 +250,12 @@ func TestTumblingKeepsAngularMomentum(t *testing.T) { flipped = true } }) - if drift := momentum().Sub(l0).Len() / l0.Len(); drift > 0.01 { - t.Errorf("angular momentum drifted by %.2f%% over 10 s", drift*100) + // the gyroscopic torque is integrated to the first order: each substep h can lose |L| (ω h)² at most, as the + // invariants (measure.gyroscopicError); over 10 s, T ω² h. Measured: 30 times under the bound, linear in h + h := sceneDt / sceneSubsteps + bound := 10 * box.AngularVelocity.LenSqr() * h + if drift := momentum().Sub(l0).Len() / l0.Len(); drift > bound { + t.Errorf("angular momentum drifted by %.2f%% over 10 s, bound %.1f%%", drift*100, bound*100) } if e := energy(); e > e0*1.001 || e < e0*0.9 { t.Errorf("rotational energy went from %.4f to %.4f J", e0, e) @@ -270,7 +274,7 @@ func TestDamping(t *testing.T) { ball.Velocity, ball.AngularVelocity = mgl64.Vec3{1, 0, 0}, mgl64.Vec3{0, 0, 1} simulate(w, 1, nil) h := sceneDt / sceneSubsteps - steps := float64(50 * sceneSubsteps) + steps := math.Round(1/sceneDt) * sceneSubsteps if want := math.Pow(1/(1+h*0.5), steps); math.Abs(ball.Velocity.X()-want) > 1e-9 { t.Errorf("linear speed %.6f, want %.6f", ball.Velocity.X(), want) }