diff --git a/crates/bevy_xpbd_2d/Cargo.toml b/crates/bevy_xpbd_2d/Cargo.toml index a351260b..9cb43641 100644 --- a/crates/bevy_xpbd_2d/Cargo.toml +++ b/crates/bevy_xpbd_2d/Cargo.toml @@ -46,11 +46,13 @@ required-features = ["2d"] [dependencies] bevy_xpbd_derive = { path = "../bevy_xpbd_derive", version = "0.1" } -bevy = { version = "0.14.0-rc" } +bevy = { version = "0.14.0-rc", default-features = false } bevy_math = { version = "0.14.0-rc" } parry2d = { version = "0.15", optional = true } parry2d-f64 = { version = "0.15", optional = true } -nalgebra = { version = "0.32.6", features = ["convert-glam027"], optional = true } +nalgebra = { version = "0.32.6", features = [ + "convert-glam027", +], optional = true } serde = { version = "1", features = ["derive"], optional = true } derive_more = "0.99" indexmap = "2.0.0" diff --git a/crates/bevy_xpbd_3d/Cargo.toml b/crates/bevy_xpbd_3d/Cargo.toml index 150ccc52..c995d3ce 100644 --- a/crates/bevy_xpbd_3d/Cargo.toml +++ b/crates/bevy_xpbd_3d/Cargo.toml @@ -55,11 +55,13 @@ required-features = ["3d"] [dependencies] bevy_xpbd_derive = { path = "../bevy_xpbd_derive", version = "0.1" } -bevy = { version = "0.14.0-rc" } +bevy = { version = "0.14.0-rc", default-features = false } bevy_math = { version = "0.14.0-rc" } parry3d = { version = "0.15", optional = true } parry3d-f64 = { version = "0.15", optional = true } -nalgebra = { version = "0.32.6", features = ["convert-glam027"], optional = true } +nalgebra = { version = "0.32.6", features = [ + "convert-glam027", +], optional = true } serde = { version = "1", features = ["derive"], optional = true } derive_more = "0.99" indexmap = "2.0.0" diff --git a/crates/bevy_xpbd_3d/snapshots/bevy_xpbd_3d__tests__cubes_simulation_is_deterministic_across_machines.snap b/crates/bevy_xpbd_3d/snapshots/bevy_xpbd_3d__tests__cubes_simulation_is_deterministic_across_machines.snap index 164fba50..a59e89ab 100644 --- a/crates/bevy_xpbd_3d/snapshots/bevy_xpbd_3d__tests__cubes_simulation_is_deterministic_across_machines.snap +++ b/crates/bevy_xpbd_3d/snapshots/bevy_xpbd_3d__tests__cubes_simulation_is_deterministic_across_machines.snap @@ -9,15 +9,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.3006864, - 0.49994802, - -5.7989707, + -4.2617664, + 0.4999546, + -5.850172, ), rotation: Quat( - 2.5563864e-5, - -0.13944258, - 6.5661366e-6, - 0.99023014, + 3.573122e-5, + -0.1058252, + 1.9138555e-5, + 0.99438477, ), scale: Vec3( 1.0, @@ -32,15 +32,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.34125, - 0.49998254, - -2.520743, + -4.3064404, + 0.4999569, + -2.6481085, ), rotation: Quat( - 7.254554e-6, - -0.16648017, - -4.946743e-6, - 0.9860448, + 6.661155e-6, + -0.14438927, + 3.970238e-6, + 0.98952097, ), scale: Vec3( 1.0, @@ -55,15 +55,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.4208765, - 0.49995813, - 0.065870434, + -4.3218145, + 0.49994364, + -0.009704462, ), rotation: Quat( - 6.7530023e-6, - -0.09734187, - -4.4243625e-6, - 0.995251, + 7.008083e-6, + -0.1467056, + -4.7925746e-6, + 0.9891802, ), scale: Vec3( 1.0, @@ -78,15 +78,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.44458, - 0.49998608, - 2.3962872, + -4.320339, + 0.49994954, + 2.221934, ), rotation: Quat( - -3.862845e-6, - -0.08763834, - 1.2271895e-6, - 0.99615234, + 2.3050113e-6, + -0.18636058, + -2.0008142e-7, + 0.9824814, ), scale: Vec3( 1.0, @@ -101,15 +101,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.9637036, - 0.49994987, - -5.7516756, + -1.9012831, + 0.49996313, + -5.747129, ), rotation: Quat( - 3.7028516e-5, - 0.010398579, - 5.0167346e-6, - 0.99994594, + 1.7088601e-5, + -0.049813338, + -1.4914843e-6, + 0.99875855, ), scale: Vec3( 1.0, @@ -124,15 +124,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.9832739, - 0.49993584, - -2.382758, + -1.9221903, + 0.49992433, + -2.5490732, ), rotation: Quat( - 3.0113886e-6, - -0.12687473, - 4.0704103e-6, - 0.99191874, + 9.3676135e-7, + -0.0910071, + 1.1568856e-5, + 0.99585027, ), scale: Vec3( 1.0, @@ -147,15 +147,15 @@ expression: bodies ), Transform { translation: Vec3( - -2.1935527, - 0.49994648, - -0.13222764, + -2.2033687, + 0.49994245, + -0.0414518, ), rotation: Quat( - 1.1267305e-5, - -0.15084976, - -1.4297254e-5, - 0.9885567, + 1.2148819e-5, + -0.1344137, + 7.486361e-7, + 0.9909253, ), scale: Vec3( 1.0, @@ -170,15 +170,15 @@ expression: bodies ), Transform { translation: Vec3( - -2.190492, - 0.49995232, - 2.3649914, + -2.195145, + 0.49994546, + 2.4222329, ), rotation: Quat( - 1.0206272e-6, - -0.15634996, - 5.843247e-7, - 0.9877017, + 9.179024e-6, + -0.1664576, + 8.669787e-6, + 0.98604864, ), scale: Vec3( 1.0, @@ -193,15 +193,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.50485873, - 0.49994126, - -5.537582, + 0.50737727, + 0.49997383, + -5.5371494, ), rotation: Quat( - 4.887261e-5, - 0.011927954, - 9.431508e-6, - 0.99992883, + 9.78547e-6, + -0.047478247, + -9.398216e-7, + 0.9988723, ), scale: Vec3( 1.0, @@ -216,15 +216,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.28479943, - 0.4999536, - -2.253131, + 0.2998932, + 0.49997103, + -2.3289566, ), rotation: Quat( - 8.674237e-6, - -0.045467205, - 1.5355862e-6, - 0.99896586, + -1.185439e-5, + -0.110907756, + 6.907667e-6, + 0.9938307, ), scale: Vec3( 1.0, @@ -239,15 +239,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.40823856, - 0.49993768, - 0.0067982823, + 0.069818676, + 0.4999351, + 0.017867053, ), rotation: Quat( - 5.3277486e-6, - -0.14052378, - -2.3644911e-6, - 0.9900773, + 8.786237e-6, + -0.16995092, + 6.933531e-6, + 0.98545253, ), scale: Vec3( 1.0, @@ -262,15 +262,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.07479347, - 0.499951, - 2.385635, + 0.16879787, + 0.4999593, + 2.4852552, ), rotation: Quat( - -3.00183e-7, - -0.18080756, - 5.313273e-6, - 0.9835185, + 7.854049e-6, + -0.1357702, + 7.512223e-6, + 0.99074036, ), scale: Vec3( 1.0, @@ -285,15 +285,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.9356205, - 0.49996716, - -5.293405, + 2.8243964, + 0.4999569, + -5.2702985, ), rotation: Quat( - 1.9918578e-5, - -0.18862937, - -7.567649e-6, - 0.98204833, + 1.8391645e-5, + -0.23051743, + 6.320918e-6, + 0.9730682, ), scale: Vec3( 1.0, @@ -308,15 +308,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.5417275, - 0.4999344, - -2.4746914, + 2.463946, + 0.49993855, + -2.4356751, ), rotation: Quat( - 7.780524e-6, - -0.13873443, - -2.803678e-6, - 0.9903296, + 8.469337e-6, + -0.11271293, + -3.552528e-6, + 0.9936276, ), scale: Vec3( 1.0, @@ -331,15 +331,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.4991288, - 0.49994004, - -0.020369604, + 2.4887574, + 0.49993554, + 0.0004435418, ), rotation: Quat( - 6.169427e-6, - -0.13957575, - -2.4661904e-6, - 0.99021137, + 2.515169e-6, + -0.14334317, + 8.402555e-7, + 0.989673, ), scale: Vec3( 1.0, @@ -354,15 +354,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.222926, - 0.49993682, - 2.491799, + 2.3951437, + 0.49993062, + 2.4571276, ), rotation: Quat( - 1.9152405e-6, - -0.12406718, - 1.5702897e-6, - 0.9922738, + 2.007865e-6, + -0.12371828, + 4.269886e-6, + 0.9923174, ), scale: Vec3( 1.0, @@ -377,15 +377,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.802956, - 2.4998732, - -4.9959145, + -4.7609425, + 2.4998963, + -4.9909444, ), rotation: Quat( - 3.4058918e-5, - -0.02808738, - 1.5389675e-6, - 0.9996055, + 7.0230475e-5, + -0.0777875, + 1.4330533e-5, + 0.99696994, ), scale: Vec3( 1.0, @@ -400,15 +400,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.1267033, - 2.4999716, - -2.1380875, + -4.0055633, + 2.4999528, + -2.284876, ), rotation: Quat( - 9.902313e-6, - -0.04106795, - -4.502343e-6, - 0.99915636, + 7.5023313e-6, + 0.016935397, + 6.621797e-6, + 0.9998566, ), scale: Vec3( 1.0, @@ -423,15 +423,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.4816256, - 2.4999707, - 0.54315186, + -4.1989307, + 2.4999678, + 0.024624022, ), rotation: Quat( - -6.963642e-6, - 0.07485945, - -7.725867e-6, - 0.9971941, + 1.2540428e-5, + 0.012859379, + -1.4782652e-5, + 0.9999173, ), scale: Vec3( 1.0, @@ -446,15 +446,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.479078, - 0.49999946, - 5.250012, + -4.1972713, + 2.4999049, + 2.5970695, ), rotation: Quat( - 0.707078, - -0.0064172293, - 0.0064162086, - 0.7070773, + 2.8318186e-6, + -0.084395126, + 1.6209724e-6, + 0.99643236, ), scale: Vec3( 1.0, @@ -469,15 +469,15 @@ expression: bodies ), Transform { translation: Vec3( - -2.0833967, - 2.4998305, - -4.6686044, + -2.0582845, + 2.499936, + -4.697543, ), rotation: Quat( - 9.812366e-5, - -0.04694147, - -8.079542e-6, - 0.9988976, + 1.6334006e-5, + 0.011563025, + -5.4914335e-6, + 0.9999331, ), scale: Vec3( 1.0, @@ -492,15 +492,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.9658682, - 2.4999316, - -2.5046182, + -1.9788162, + 2.4999409, + -2.3972607, ), rotation: Quat( - 2.3796088e-6, - 0.00437061, - 4.3683176e-6, - 0.99999046, + 6.6024595e-6, + 0.02362521, + -8.801972e-6, + 0.9997209, ), scale: Vec3( 1.0, @@ -515,15 +515,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.9538839, - 2.4999335, - -0.19311312, + -1.9118792, + 2.499907, + -0.23819661, ), rotation: Quat( - 2.18081e-5, - 0.04672425, - -2.6875077e-5, - 0.9989078, + 9.377828e-6, + -0.003039026, + -4.9543423e-6, + 0.9999954, ), scale: Vec3( 1.0, @@ -538,15 +538,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.6183211, - 2.4999154, - 2.213647, + -1.791303, + 2.4998999, + 2.3251147, ), rotation: Quat( - 3.5388694e-6, - -0.07203751, - 8.012456e-6, - 0.99740195, + 8.668525e-6, + -0.08713609, + 1.1167676e-5, + 0.9961964, ), scale: Vec3( 1.0, @@ -561,15 +561,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.24880564, - 2.4998293, - -4.4588413, + 0.16339311, + 2.4999661, + -4.1933465, ), rotation: Quat( - 0.000113148395, - 0.04139323, - -9.658961e-6, - 0.99914294, + 5.112969e-6, + 0.09229462, + -5.7478887e-6, + 0.9957318, ), scale: Vec3( 1.0, @@ -584,15 +584,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.18976483, - 2.4999537, - -2.0090744, + 0.19633481, + 2.4998996, + -1.9324025, ), rotation: Quat( - 1.4431914e-6, - 0.021697124, - -3.6956703e-6, - 0.99976456, + -2.987723e-5, + -0.0044867536, + -1.2633382e-5, + 0.9999899, ), scale: Vec3( 1.0, @@ -607,15 +607,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.30283442, - 2.4999504, - 0.24991783, + 0.13126238, + 2.49993, + 0.1477116, ), rotation: Quat( - -5.709594e-6, - 0.008069546, - -8.824132e-6, - 0.99996746, + 2.4705829e-5, + 0.017267318, + -5.882915e-6, + 0.9998509, ), scale: Vec3( 1.0, @@ -630,15 +630,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.49840316, - 2.4999552, - 2.386049, + 0.57213444, + 2.4999595, + 2.3758326, ), rotation: Quat( - -1.9563176e-6, - -0.008069459, - 4.03903e-6, - 0.99996746, + 2.2793906e-6, + -0.06613546, + -8.770111e-6, + 0.99781066, ), scale: Vec3( 1.0, @@ -653,15 +653,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.7958717, - 2.4999468, - -4.9847417, + 2.6940353, + 2.4999282, + -4.8833747, ), rotation: Quat( - 2.0935327e-5, - -0.02026101, - -1.0689646e-5, - 0.9997947, + 5.989609e-6, + -0.017840687, + 7.0188667e-6, + 0.99984086, ), scale: Vec3( 1.0, @@ -676,15 +676,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.53482, - 2.499917, - -2.8631797, + 2.4808466, + 2.4999146, + -2.7586346, ), rotation: Quat( - -9.716486e-6, - -0.057248715, - 1.7167887e-6, - 0.9983599, + 6.2101612e-6, + -0.0046678404, + -6.3536993e-7, + 0.9999891, ), scale: Vec3( 1.0, @@ -699,15 +699,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.4877746, - 2.4999106, - -0.6242965, + 2.469826, + 2.4999118, + -0.4482274, ), rotation: Quat( - 7.5665224e-7, - -0.04872094, - 9.191748e-7, - 0.99881244, + -4.6545406e-7, + -0.04133346, + 4.724464e-6, + 0.9991454, ), scale: Vec3( 1.0, @@ -722,15 +722,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.7696772, - 2.4999475, - 1.8043613, + 2.7965014, + 2.499943, + 1.8568091, ), rotation: Quat( - 2.1536397e-7, - 0.090809226, - -8.4199455e-6, - 0.9958683, + -3.3913225e-6, + 0.10728257, + -1.5470909e-5, + 0.99422854, ), scale: Vec3( 1.0, @@ -745,15 +745,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.386225, - 4.499904, - -5.483672, + -4.363254, + 4.4997783, + -4.618845, ), rotation: Quat( - 3.1910626e-5, - 0.019788539, - 6.922895e-8, - 0.9998042, + 6.461376e-5, + -0.010752076, + 1.6418633e-5, + 0.9999422, ), scale: Vec3( 1.0, @@ -768,15 +768,15 @@ expression: bodies ), Transform { translation: Vec3( - -7.7358646, - 0.49999946, - -2.0709054, + -4.2724934, + 4.499878, + -2.168243, ), rotation: Quat( - -0.1501146, - -6.5004235e-7, - 0.9886686, - -5.542554e-7, + 2.621277e-6, + 0.065876156, + 5.5477135e-6, + 0.9978278, ), scale: Vec3( 1.0, @@ -791,15 +791,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.471921, - 4.499904, - 0.75961554, + -4.077332, + 4.4998717, + 0.10678628, ), rotation: Quat( - -4.0262685e-6, - -0.024324974, - -6.990839e-6, - 0.9997041, + 1.9025832e-5, + 0.041490078, + -2.3065013e-5, + 0.9991389, ), scale: Vec3( 1.0, @@ -814,15 +814,15 @@ expression: bodies ), Transform { translation: Vec3( - -5.6175113, - 0.49999946, - 8.242992, + -3.911725, + 4.4998875, + 2.4555938, ), rotation: Quat( - 0.6893942, - -0.15727805, - 0.1572772, - 0.68939334, + 3.2655569e-6, + 0.058701254, + 1.8361212e-6, + 0.9982756, ), scale: Vec3( 1.0, @@ -837,15 +837,15 @@ expression: bodies ), Transform { translation: Vec3( - -2.0943875, - 4.4998355, - -4.623846, + -2.2815921, + 4.49988, + -4.8674803, ), rotation: Quat( - 0.00010044623, - 0.012530968, - -8.938792e-6, - 0.9999215, + 1.5805883e-5, + 0.039157994, + 1.2015109e-6, + 0.999233, ), scale: Vec3( 1.0, @@ -860,15 +860,15 @@ expression: bodies ), Transform { translation: Vec3( - -2.009536, - 4.499907, - -2.4300594, + -2.0591104, + 4.499893, + -2.3643444, ), rotation: Quat( - -2.0774619e-6, - 0.014378485, - -5.443816e-6, - 0.99989665, + 8.285662e-6, + 0.06541873, + -1.2651373e-5, + 0.9978579, ), scale: Vec3( 1.0, @@ -883,15 +883,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.9585384, - 4.499923, - -0.22404596, + -1.9292995, + 4.4999027, + -0.039117157, ), rotation: Quat( - 1.715594e-5, - 0.014164529, - -3.7965892e-5, - 0.9998997, + -4.5387683e-6, + 0.022030165, + -1.2132288e-5, + 0.9997573, ), scale: Vec3( 1.0, @@ -906,15 +906,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.8411386, - 4.4998856, - 2.3135095, + -1.8594667, + 4.499914, + 2.1716297, ), rotation: Quat( - -3.3378979e-6, - -0.056289833, - 1.8587364e-6, - 0.99841446, + 8.503707e-6, + 0.031846423, + 7.0398482e-6, + 0.99949276, ), scale: Vec3( 1.0, @@ -929,15 +929,15 @@ expression: bodies ), Transform { translation: Vec3( - -0.02375713, - 4.4996953, - -4.2030654, + -0.089406155, + 4.4999027, + -4.0922565, ), rotation: Quat( - 0.00011672663, - 0.03757818, - -1.2387409e-5, - 0.9992937, + -1.683093e-7, + 0.013369354, + -7.971003e-6, + 0.99991065, ), scale: Vec3( 1.0, @@ -952,15 +952,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.119919084, - 4.4998827, - -2.0700102, + 0.0452622, + 4.499876, + -1.9659212, ), rotation: Quat( - 4.0283912e-6, - 0.05287602, - -5.329639e-6, - 0.9986011, + -2.1662156e-5, + 0.019904593, + -1.30407725e-5, + 0.9998019, ), scale: Vec3( 1.0, @@ -975,15 +975,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.19294138, - 4.499873, - 0.22602274, + 0.17919067, + 4.499906, + 0.094455205, ), rotation: Quat( - 2.7269325e-6, - 0.010626739, - -1.257475e-5, - 0.99994355, + 2.09671e-5, + 0.036664918, + -1.413997e-5, + 0.9993276, ), scale: Vec3( 1.0, @@ -998,15 +998,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.31366748, - 4.4999237, - 2.7180665, + 0.20144333, + 4.4998794, + 3.066769, ), rotation: Quat( - 5.3005783e-6, - -0.06315649, - -1.3187989e-5, - 0.99800366, + 1.7986804e-5, + 0.022055857, + -1.0094973e-5, + 0.99975675, ), scale: Vec3( 1.0, @@ -1021,15 +1021,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.377476, - 4.4999547, - -4.8546257, + 2.5093102, + 4.499909, + -4.6222143, ), rotation: Quat( - 2.7817585e-5, - 0.17257857, - -1.4667208e-5, - 0.9849958, + 3.0352854e-7, + 0.107366234, + 6.1290875e-6, + 0.99421954, ), scale: Vec3( 1.0, @@ -1044,15 +1044,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.314021, - 4.499904, - -2.3202064, + 2.3912914, + 4.499895, + -2.2906146, ), rotation: Quat( - -1.2845678e-5, - 0.01641865, - -3.9993383e-6, - 0.99986523, + -2.7984424e-6, + 0.04843786, + -4.968584e-6, + 0.9988262, ), scale: Vec3( 1.0, @@ -1067,15 +1067,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.4601562, - 4.499923, - 0.03035463, + 2.4207766, + 4.499921, + -0.057150625, ), rotation: Quat( - -1.0146865e-5, - 0.042424247, - 9.197388e-8, - 0.9990997, + -8.362876e-6, + 0.014766381, + 1.4427535e-6, + 0.999891, ), scale: Vec3( 1.0, @@ -1090,15 +1090,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.5232496, - 4.499871, - 2.199358, + 2.6593442, + 4.4998827, + 2.4034224, ), rotation: Quat( - 1.0287707e-5, - 0.027183056, - -1.0482277e-5, - 0.99963045, + 2.2390845e-5, + 0.06742529, + -2.196313e-5, + 0.99772435, ), scale: Vec3( 1.0, @@ -1113,15 +1113,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.798367, - 6.499823, - -4.896724, + -4.7637234, + 6.499861, + -5.340199, ), rotation: Quat( - 4.156623e-5, - -0.18305051, - -4.9355835e-6, - 0.9831035, + 6.481955e-5, + -0.08735548, + 1.0994606e-5, + 0.9961772, ), scale: Vec3( 1.0, @@ -1136,15 +1136,15 @@ expression: bodies ), Transform { translation: Vec3( - -13.893423, - 0.49999946, - -3.619525, + -4.192832, + 6.499882, + -1.9592813, ), rotation: Quat( - -0.51768076, - -0.517681, - 0.48167115, - 0.48166996, + 1.6710153e-6, + -0.021386456, + 7.192534e-6, + 0.9997713, ), scale: Vec3( 1.0, @@ -1159,15 +1159,15 @@ expression: bodies ), Transform { translation: Vec3( - -4.381537, - 6.4999037, - 0.48308283, + -4.17173, + 6.4998856, + 0.066886045, ), rotation: Quat( - -9.016728e-6, - -0.07982172, - -8.499087e-6, - 0.9968092, + 2.3180755e-5, + -0.033207588, + -2.7147373e-5, + 0.9994485, ), scale: Vec3( 1.0, @@ -1182,15 +1182,15 @@ expression: bodies ), Transform { translation: Vec3( - -8.318473, - 0.49999946, - 19.172176, + -4.1792717, + 6.4998884, + 2.1878312, ), rotation: Quat( - 0.6616829, - -0.24935219, - 0.24935146, - 0.66168195, + 5.4603147e-6, + -0.029115543, + 1.8338168e-6, + 0.99957603, ), scale: Vec3( 1.0, @@ -1205,15 +1205,15 @@ expression: bodies ), Transform { translation: Vec3( - -2.1034138, - 6.4998574, - -4.8453918, + -2.1228366, + 6.4999056, + -4.3379493, ), rotation: Quat( - 9.41463e-5, - 0.0820297, - 6.9586326e-6, - 0.9966299, + -7.165126e-6, + 0.03422352, + 3.68079e-6, + 0.9994142, ), scale: Vec3( 1.0, @@ -1228,15 +1228,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.9645659, - 6.49991, - -2.2860827, + -1.9883639, + 6.4999, + -1.9881402, ), rotation: Quat( - -1.9683644e-6, - -0.021907628, - -3.0639426e-6, - 0.99976, + 1.3898177e-6, + -0.014251919, + -1.0656904e-5, + 0.99989843, ), scale: Vec3( 1.0, @@ -1251,15 +1251,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.9352555, - 6.4999294, - -0.2553877, + -2.094955, + 6.4998703, + 0.032412723, ), rotation: Quat( - 1.7038174e-5, - -0.035811152, - -4.1647163e-5, - 0.9993586, + -5.394439e-6, + -0.026065364, + -1.2520514e-5, + 0.99966025, ), scale: Vec3( 1.0, @@ -1274,15 +1274,15 @@ expression: bodies ), Transform { translation: Vec3( - -1.949988, - 6.4998813, - 2.0137444, + -1.8841723, + 6.4998913, + 2.2741911, ), rotation: Quat( - 4.7868593e-7, - -0.004052656, - -2.5726246e-8, - 0.9999918, + 7.136536e-6, + -0.08494512, + -5.5456294e-6, + 0.99638563, ), scale: Vec3( 1.0, @@ -1297,15 +1297,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.08226854, - 6.4997396, - -4.4143085, + -0.040435012, + 6.499896, + -4.217033, ), rotation: Quat( - 0.000111104484, - -0.022358429, - -2.2984656e-5, - 0.99975, + -1.0937498e-6, + 0.0005769148, + -1.2361402e-5, + 0.9999998, ), scale: Vec3( 1.0, @@ -1320,15 +1320,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.043099843, - 6.49989, - -2.2432802, + 0.08133389, + 6.49987, + -1.9905888, ), rotation: Quat( - -4.881092e-7, - -0.017938888, - -5.358564e-6, - 0.99983907, + -2.3703553e-5, + 0.021437174, + -1.7860051e-5, + 0.9997702, ), scale: Vec3( 1.0, @@ -1343,15 +1343,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.20691219, - 6.499881, - 0.23858999, + 0.1879624, + 6.4999003, + 0.12204986, ), rotation: Quat( - 3.7870827e-6, - -0.06124742, - -1.658932e-5, - 0.99812263, + 2.5173955e-5, + 0.034547277, + -1.651468e-5, + 0.99940306, ), scale: Vec3( 1.0, @@ -1366,15 +1366,15 @@ expression: bodies ), Transform { translation: Vec3( - 0.15775675, - 6.499881, - 3.053992, + 0.17312475, + 6.499889, + 2.8727741, ), rotation: Quat( - 7.822202e-6, - -0.11158954, - -1.5739672e-5, - 0.9937544, + 1.1870379e-5, + -0.039233103, + -1.1666968e-5, + 0.9992301, ), scale: Vec3( 1.0, @@ -1389,15 +1389,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.31714, - 6.499921, - -4.240581, + 2.1174662, + 6.4999065, + -4.188167, ), rotation: Quat( - 1.8280816e-5, - 0.020745432, - -6.7741507e-6, - 0.99978477, + -2.7270207e-6, + -0.020572882, + 3.1483255e-6, + 0.99978834, ), scale: Vec3( 1.0, @@ -1412,15 +1412,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.0382423, - 6.4999146, - -2.0699782, + 2.610953, + 6.499894, + -2.0543513, ), rotation: Quat( - -1.1182783e-5, - -0.017901609, - -5.2746063e-6, - 0.9998398, + -2.1848605e-6, + -0.0671244, + -6.317318e-6, + 0.9977446, ), scale: Vec3( 1.0, @@ -1435,15 +1435,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.3031816, - 6.499888, - -0.0062856125, + 2.3809884, + 6.4999104, + 0.34574062, ), rotation: Quat( - -1.2922834e-5, - -0.0152022885, - 5.0969556e-6, - 0.9998844, + -1.4357044e-5, + 0.06815255, + 1.5165259e-7, + 0.9976749, ), scale: Vec3( 1.0, @@ -1458,15 +1458,15 @@ expression: bodies ), Transform { translation: Vec3( - 2.482787, - 6.4998736, - 2.4378264, + 2.8196137, + 6.4998636, + 2.6008103, ), rotation: Quat( - 7.338764e-6, - -0.144237, - -1.5116398e-5, - 0.9895432, + 2.482447e-5, + 0.072949596, + -2.2727407e-5, + 0.9973356, ), scale: Vec3( 1.0, diff --git a/src/plugins/collision/collider_backend.rs b/src/plugins/collision/collider/backend.rs similarity index 54% rename from src/plugins/collision/collider_backend.rs rename to src/plugins/collision/collider/backend.rs index 8a070230..e712d9f9 100644 --- a/src/plugins/collision/collider_backend.rs +++ b/src/plugins/collision/collider/backend.rs @@ -4,12 +4,7 @@ use std::marker::PhantomData; -use crate::{ - broad_phase::BroadPhaseSet, - prelude::*, - prepare::{match_any, PrepareSet}, - sync::SyncSet, -}; +use crate::{broad_phase::BroadPhaseSet, prelude::*, prepare::PrepareSet}; #[cfg(all( feature = "3d", feature = "async-collider", @@ -23,17 +18,16 @@ use bevy::{ /// A plugin for handling generic collider backend logic. /// -/// - Initializes colliders. +/// - Initializes colliders, including [`AsyncCollider`] and [`AsyncSceneCollider`]. /// - Updates [`ColliderAabb`]s. -/// - Updates [`ColliderParent`]s. -/// - Updates collider mass properties. -/// - Updates collider scale based on `Transform` scale. -/// - Propagates child collider positions. +/// - Updates collider mass properties, also updating rigid bodies accordingly. +/// +/// This plugin should typically be used together with the [`ColliderHierarchyPlugin`]. /// /// ## Custom collision backends /// /// By default, [`PhysicsPlugins`] adds this plugin for the [`Collider`] component. -/// You can also create custom collider backends by implementing the [`AnyCollider`] and [`ScalableCollider`] traits. +/// You can also create custom collider backends by implementing the [`AnyCollider`] trait for a type. /// /// To use a custom collider backend, simply add the [`ColliderBackendPlugin`] with your collider type: /// @@ -60,9 +54,12 @@ use bevy::{ /// } /// ``` /// -/// Assuming you have implemented [`AnyCollider`] and [`ScalableCollider`] correctly, +/// Assuming you have implemented [`AnyCollider`] correctly, /// it should now work with the rest of the engine just like normal [`Collider`]s! /// +/// Remember to also add the [`ColliderHierarchyPlugin`] for your custom collider +/// type if you want transforms to work for them. +/// /// **Note**: [Spatial queries](spatial_query) are not supported for custom colliders yet. pub struct ColliderBackendPlugin { @@ -128,52 +125,16 @@ impl Plugin for ColliderBackendPlugin { ); }); - // Run transform propagation if new colliders without rigid bodies have been added. - // The `PreparePlugin` should handle transform propagation for new rigid bodies. - app.add_systems( - self.schedule, - (( - bevy::transform::systems::sync_simple_transforms, - bevy::transform::systems::propagate_transforms, - ) - .chain() - .run_if(match_any::<(Added, Without)>),) - .chain() - .in_set(PrepareSet::PropagateTransforms) - // Allowing ambiguities is required so that it's possible - // to have multiple collision backends at the same time. - .ambiguous_with_all(), - ); - app.add_systems( self.schedule, ( - ( - init_colliders::, - apply_deferred, - update_collider_parents::, - apply_deferred, - ) - .chain() - .in_set(PrepareSet::InitColliders), + init_colliders::.in_set(PrepareSet::InitColliders), init_transforms:: .in_set(PrepareSet::InitTransforms) .after(init_transforms::), - ( - ( - propagate_collider_transforms, - update_child_collider_position, - ) - .chain() - .run_if(match_any::>), - update_collider_mass_properties::, - ) - .chain() + update_collider_mass_properties:: .in_set(PrepareSet::Finalize) .before(prepare::update_mass_properties), - update_collider_scale:: - .after(SyncSet::Update) - .before(SyncSet::Last), ), ); @@ -191,40 +152,18 @@ impl Plugin for ColliderBackendPlugin { .ambiguous_with_all(), ); - physics_schedule - .add_systems(handle_rigid_body_removals.after(PhysicsStepSet::SpatialQuery)); - #[cfg(all( feature = "3d", feature = "async-collider", feature = "default-collider" ))] app.add_systems(Update, (init_async_colliders, init_async_scene_colliders)); - - // Update child colliders before narrow phase in substepping loop - let substep_schedule = app - .get_schedule_mut(SubstepSchedule) - .expect("add SubstepSchedule first"); - substep_schedule.add_systems( - ( - propagate_collider_transforms, - update_child_collider_position, - ) - .chain() - .after(SubstepSet::Integrate) - .before(SubstepSet::NarrowPhase) - .ambiguous_with_all(), - ); } } -#[derive(Reflect, Clone, Copy, Component, Debug, Default, Deref, DerefMut, PartialEq)] -#[reflect(Component)] -pub(crate) struct PreviousColliderTransform(ColliderTransform); - /// Initializes missing components for [colliders](Collider). #[allow(clippy::type_complexity)] -fn init_colliders( +pub(crate) fn init_colliders( mut commands: Commands, mut colliders: Query< ( @@ -455,43 +394,6 @@ fn update_aabb( } } -#[allow(clippy::type_complexity)] -fn update_collider_parents( - mut commands: Commands, - mut bodies: Query<(Entity, Option<&mut ColliderParent>, Has), With>, - children: Query<&Children>, - mut child_colliders: Query, (With, Without)>, -) { - for (entity, collider_parent, has_collider) in &mut bodies { - if has_collider { - if let Some(mut collider_parent) = collider_parent { - collider_parent.0 = entity; - } else { - commands.entity(entity).try_insert(( - ColliderParent(entity), - // Todo: This probably causes a one frame delay. Compute real value? - ColliderTransform::default(), - PreviousColliderTransform::default(), - )); - } - } - for child in children.iter_descendants(entity) { - if let Ok(collider_parent) = child_colliders.get_mut(child) { - if let Some(mut collider_parent) = collider_parent { - collider_parent.0 = entity; - } else { - commands.entity(child).insert(( - ColliderParent(entity), - // Todo: This probably causes a one frame delay. Compute real value? - ColliderTransform::default(), - PreviousColliderTransform::default(), - )); - } - } - } - } -} - /// A resource that stores the system ID for the system that reacts to collider removals. #[derive(Resource)] struct ColliderRemovalSystem(SystemId<(ColliderParent, ColliderMassProperties, ColliderTransform)>); @@ -524,278 +426,9 @@ fn collider_removed( } } -/// Updates colliders when the rigid bodies they were attached to have been removed. -fn handle_rigid_body_removals( - mut commands: Commands, - colliders: Query<(Entity, &ColliderParent), Without>, - bodies: Query<(), With>, - removals: RemovedComponents, -) { - // Return if no rigid bodies have been removed - if removals.is_empty() { - return; - } - - for (collider_entity, collider_parent) in &colliders { - // If the body associated with the collider parent entity doesn't exist, - // remove ColliderParent and ColliderTransform. - if !bodies.contains(collider_parent.get()) { - commands.entity(collider_entity).remove::<( - ColliderParent, - ColliderTransform, - PreviousColliderTransform, - )>(); - } - } -} - -#[allow(clippy::type_complexity)] -pub(crate) fn update_child_collider_position( - mut colliders: Query< - ( - &ColliderTransform, - &mut Position, - &mut Rotation, - &ColliderParent, - ), - Without, - >, - parents: Query<(&Position, &Rotation), (With, With)>, -) { - for (collider_transform, mut position, mut rotation, parent) in &mut colliders { - let Ok((parent_pos, parent_rot)) = parents.get(parent.get()) else { - continue; - }; - - position.0 = parent_pos.0 + parent_rot.rotate(collider_transform.translation); - #[cfg(feature = "2d")] - { - *rotation = *parent_rot + collider_transform.rotation; - } - #[cfg(feature = "3d")] - { - *rotation = (parent_rot.0 * collider_transform.rotation.0) - .normalize() - .into(); - } - } -} - -/// Updates the scale of colliders based on [`Transform`] scale. -#[allow(clippy::type_complexity)] -pub fn update_collider_scale( - mut colliders: ParamSet<( - // Root bodies - Query<(&Transform, &mut C), Without>, - // Child colliders - Query<(&ColliderTransform, &mut C), With>, - )>, -) { - // Update collider scale for root bodies - for (transform, mut collider) in &mut colliders.p0() { - #[cfg(feature = "2d")] - let scale = transform.scale.truncate().adjust_precision(); - #[cfg(feature = "3d")] - let scale = transform.scale.adjust_precision(); - if scale != collider.scale() { - // TODO: Support configurable subdivision count for shapes that - // can't be represented without approximations after scaling. - collider.set_scale(scale, 10); - } - } - - // Update collider scale for child colliders - for (collider_transform, mut collider) in &mut colliders.p1() { - if collider_transform.scale != collider.scale() { - // TODO: Support configurable subdivision count for shapes that - // can't be represented without approximations after scaling. - collider.set_scale(collider_transform.scale, 10); - } - } -} - -/// Updates [`ColliderTransform`]s based on entity hierarchies. Each transform is computed by recursively -/// traversing the children of each rigid body and adding their transforms together to form -/// the total transform relative to the body. -/// -/// This is largely a clone of `propagate_transforms` in `bevy_transform`. -#[allow(clippy::type_complexity)] -pub(crate) fn propagate_collider_transforms( - mut root_query: Query<(Entity, Ref, &Children), Without>, - collider_query: Query< - ( - Ref, - Option<&mut ColliderTransform>, - Option<&Children>, - ), - With, - >, - parent_query: Query<(Entity, Ref, Has, Ref)>, -) { - root_query.par_iter_mut().for_each( - |(entity, transform,children)| { - for (child, child_transform, is_child_rb, parent) in parent_query.iter_many(children) { - assert_eq!( - parent.get(), entity, - "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" - ); - let child_transform = ColliderTransform::from(*child_transform); - - // SAFETY: - // - `child` must have consistent parentage, or the above assertion would panic. - // Since `child` is parented to a root entity, the entire hierarchy leading to it is consistent. - // - We may operate as if all descendants are consistent, since `propagate_collider_transform_recursive` will panic before - // continuing to propagate if it encounters an entity with inconsistent parentage. - // - Since each root entity is unique and the hierarchy is consistent and forest-like, - // other root entities' `propagate_collider_transform_recursive` calls will not conflict with this one. - // - Since this is the only place where `transform_query` gets used, there will be no conflicting fetches elsewhere. - unsafe { - propagate_collider_transforms_recursive( - if is_child_rb { - ColliderTransform { - scale: child_transform.scale, - ..default() - } - } else { - let transform = ColliderTransform::from(*transform); - - ColliderTransform { - translation: transform.scale * child_transform.translation, - rotation: child_transform.rotation, - scale: (transform.scale * child_transform.scale).max(Vector::splat(Scalar::EPSILON)), - } - }, - &collider_query, - &parent_query, - child, - transform.is_changed() || parent.is_changed() - ); - } - } - }, - ); -} - -/// Recursively computes the [`ColliderTransform`] for `entity` and all of its descendants -/// by propagating transforms. -/// -/// This is largely a clone of `propagate_recursive` in `bevy_transform`. -/// -/// # Panics -/// -/// If `entity`'s descendants have a malformed hierarchy, this function will panic occur before propagating -/// the transforms of any malformed entities and their descendants. -/// -/// # Safety -/// -/// - While this function is running, `transform_query` must not have any fetches for `entity`, -/// nor any of its descendants. -/// - The caller must ensure that the hierarchy leading to `entity` -/// is well-formed and must remain as a tree or a forest. Each entity must have at most one parent. -#[allow(clippy::type_complexity)] -unsafe fn propagate_collider_transforms_recursive( - transform: ColliderTransform, - collider_query: &Query< - ( - Ref, - Option<&mut ColliderTransform>, - Option<&Children>, - ), - With, - >, - parent_query: &Query<(Entity, Ref, Has, Ref)>, - entity: Entity, - mut changed: bool, -) { - let children = { - // SAFETY: This call cannot create aliased mutable references. - // - The top level iteration parallelizes on the roots of the hierarchy. - // - The caller ensures that each child has one and only one unique parent throughout the entire - // hierarchy. - // - // For example, consider the following malformed hierarchy: - // - // A - // / \ - // B C - // \ / - // D - // - // D has two parents, B and C. If the propagation passes through C, but the Parent component on D points to B, - // the above check will panic as the origin parent does match the recorded parent. - // - // Also consider the following case, where A and B are roots: - // - // A B - // \ / - // C D - // \ / - // E - // - // Even if these A and B start two separate tasks running in parallel, one of them will panic before attempting - // to mutably access E. - let Ok((transform_ref, collider_transform, children)) = - (unsafe { collider_query.get_unchecked(entity) }) - else { - return; - }; - - changed |= transform_ref.is_changed(); - if changed { - if let Some(mut collider_transform) = collider_transform { - if *collider_transform != transform { - *collider_transform = transform; - } - } - } - - children - }; - - let Some(children) = children else { return }; - for (child, child_transform, is_rb, parent) in parent_query.iter_many(children) { - assert_eq!( - parent.get(), entity, - "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" - ); - - let child_transform = ColliderTransform::from(*child_transform); - - // SAFETY: The caller guarantees that `transform_query` will not be fetched - // for any descendants of `entity`, so it is safe to call `propagate_collider_transforms_recursive` for each child. - // - // The above assertion ensures that each child has one and only one unique parent throughout the - // entire hierarchy. - unsafe { - propagate_collider_transforms_recursive( - if is_rb { - ColliderTransform { - scale: child_transform.scale, - ..default() - } - } else { - ColliderTransform { - translation: transform.transform_point(child_transform.translation), - #[cfg(feature = "2d")] - rotation: transform.rotation + child_transform.rotation, - #[cfg(feature = "3d")] - rotation: Rotation(transform.rotation.0 * child_transform.rotation.0), - scale: (transform.scale * child_transform.scale) - .max(Vector::splat(Scalar::EPSILON)), - } - }, - collider_query, - parent_query, - child, - changed || parent.is_changed(), - ); - } - } -} - /// Updates the mass properties of [`Collider`]s and [collider parents](ColliderParent). #[allow(clippy::type_complexity)] -fn update_collider_mass_properties( +pub(crate) fn update_collider_mass_properties( mut mass_props: Query<(Entity, MassPropertiesQuery)>, mut colliders: Query< ( diff --git a/src/plugins/collision/collider/hierarchy.rs b/src/plugins/collision/collider/hierarchy.rs new file mode 100644 index 00000000..96659757 --- /dev/null +++ b/src/plugins/collision/collider/hierarchy.rs @@ -0,0 +1,604 @@ +//! Handles transform propagation, scale updates, and [`ColliderParent`] updates for colliders. +//! +//! See [`ColliderHierarchyPlugin`]. + +use std::marker::PhantomData; + +use crate::{ + prelude::*, + prepare::{match_any, PrepareSet}, + sync::SyncSet, +}; +use bevy::{ecs::intern::Interned, prelude::*}; + +/// A plugin for managing the collider hierarchy and related updates. +/// +/// - Updates [`ColliderParent`]. +/// - Propagates [`ColliderTransform`]. +/// - Updates collider scale based on `Transform` scale. +/// +/// By default, [`PhysicsPlugins`] adds this plugin for the [`Collider`] component. +/// You can also use a custom collider backend by adding this plugin for any type +/// that implements the [`ScalableCollider`] trait. +/// +/// This plugin should typically be used together with the [`ColliderBackendPlugin`]. +pub struct ColliderHierarchyPlugin { + schedule: Interned, + _phantom: PhantomData, +} + +impl ColliderHierarchyPlugin { + /// Creates a [`ColliderHierarchyPlugin`] with the schedule that is used for running the [`PhysicsSchedule`]. + /// + /// The default schedule is `PostUpdate`. + pub fn new(schedule: impl ScheduleLabel) -> Self { + Self { + schedule: schedule.intern(), + _phantom: PhantomData, + } + } +} + +impl Default for ColliderHierarchyPlugin { + fn default() -> Self { + Self { + schedule: PostUpdate.intern(), + _phantom: PhantomData, + } + } +} + +impl Plugin for ColliderHierarchyPlugin { + fn build(&self, app: &mut App) { + // Mark ancestors of added colliders with the `ColliderAncestor` component. + // This is used to speed up `ColliderTransform` propagation. + app.add_systems( + self.schedule, + mark_collider_ancestors:: + .after(super::backend::init_colliders::) + .in_set(PrepareSet::InitColliders), + ); + + // Remove `ColliderAncestor` markers from removed colliders and their ancestors, + // until an ancestor that has other `ColliderAncestor` entities as children is encountered. + #[allow(clippy::type_complexity)] + app.observe( + |trigger: Trigger, + mut commands: Commands, + child_query: Query<&Children>, + parent_query: Query<&Parent>, + ancestor_query: Query< + (Entity, Has), + Or<(With, With)>, + >| { + let entity = trigger.entity(); + + // Iterate over ancestors, removing `ColliderAncestor` markers until + // an entity that has other `ColliderAncestor` children is encountered. + let mut previous_parent = entity; + for parent_entity in parent_query.iter_ancestors(entity) { + if let Ok(children) = child_query.get(parent_entity) { + // Keep the marker if `parent_entity` has a child that is a collider ancestor + // or a collider, but not the one that was removed. + let keep_marker = + ancestor_query + .iter_many(children) + .any(|(child, is_collider)| { + child != previous_parent || (is_collider && child != entity) + }); + + if keep_marker { + return; + } else { + commands.entity(parent_entity).remove::(); + } + } + + previous_parent = parent_entity; + } + }, + ); + + // Run transform propagation if new colliders without rigid bodies have been added. + // The `PreparePlugin` should handle transform propagation for new rigid bodies. + app.add_systems( + self.schedule, + ( + bevy::transform::systems::sync_simple_transforms, + bevy::transform::systems::propagate_transforms, + ) + .chain() + .run_if(match_any::<(Added, Without)>) + .in_set(PrepareSet::PropagateTransforms) + // Allowing ambiguities is required so that it's possible + // to have multiple collision backends at the same time. + .ambiguous_with_all(), + ); + + // Update collider parents. + app.add_systems( + self.schedule, + update_collider_parents:: + // TODO: Decouple the ordering here. + .after(super::backend::init_colliders::) + .in_set(PrepareSet::InitColliders), + ); + + // Propagate `ColliderTransform`s if there are new colliders. + // Only traverses trees with `ColliderAncestor`. + app.add_systems( + self.schedule, + ( + propagate_collider_transforms, + update_child_collider_position, + ) + .chain() + .run_if(match_any::>) + // TODO: Decouple the ordering here. + .before(super::backend::update_collider_mass_properties::) + .in_set(PrepareSet::Finalize), + ); + + // Update colliders based on the scale from `ColliderTransform`. + app.add_systems( + self.schedule, + update_collider_scale:: + .after(SyncSet::Update) + .before(SyncSet::Last), + ); + + let physics_schedule = app + .get_schedule_mut(PhysicsSchedule) + .expect("add PhysicsSchedule first"); + + physics_schedule + .add_systems(handle_rigid_body_removals.after(PhysicsStepSet::SpatialQuery)); + + let substep_schedule = app + .get_schedule_mut(SubstepSchedule) + .expect("add SubstepSchedule first"); + + // Propagate `ColliderTransform`s before narrow phase collision detection. + // Only traverses trees with `ColliderAncestor`. + substep_schedule.add_systems( + ( + propagate_collider_transforms, + update_child_collider_position, + ) + .chain() + .after(SubstepSet::Integrate) + .before(SubstepSet::NarrowPhase) + .ambiguous_with_all(), + ); + } +} + +/// A marker component that marks an entity as an ancestor of an entity with a collider. +/// +/// This is used to avoid unnecessary work when propagating transforms for colliders. +#[derive(Reflect, Clone, Copy, Component)] +pub struct ColliderAncestor; + +// TODO: This could also be an observer, but it'd need to have the appropriate filters +// and trigger for `Parent` changes, which doesn't seem to be possible yet. +// Unless we perhaps added an `OnColliderParentChanged` trigger. +/// Marks ancestors of added colliders with the `ColliderAncestor` component. +/// This is used to speed up `ColliderTransform` propagation. +#[allow(clippy::type_complexity)] +fn mark_collider_ancestors( + mut commands: Commands, + collider_query: Query, Changed)>, + parent_query: Query<&Parent>, + ancestor_query: Query<(), With>, +) { + for entity in &collider_query { + // Traverse up the tree, marking entities with `ColliderAncestor` + // until an entity that already has it is encountered. + for parent_entity in parent_query.iter_ancestors(entity) { + if ancestor_query.contains(parent_entity) { + return; + } else { + commands.entity(parent_entity).insert(ColliderAncestor); + } + } + } +} + +#[derive(Reflect, Clone, Copy, Component, Debug, Default, Deref, DerefMut, PartialEq)] +#[reflect(Component)] +pub(crate) struct PreviousColliderTransform(pub ColliderTransform); + +#[allow(clippy::type_complexity)] +fn update_collider_parents( + mut commands: Commands, + mut bodies: Query<(Entity, Option<&mut ColliderParent>, Has), With>, + children: Query<&Children>, + mut child_colliders: Query, (With, Without)>, +) { + for (entity, collider_parent, has_collider) in &mut bodies { + if has_collider { + if let Some(mut collider_parent) = collider_parent { + collider_parent.0 = entity; + } else { + commands.entity(entity).try_insert(( + ColliderParent(entity), + // Todo: This probably causes a one frame delay. Compute real value? + ColliderTransform::default(), + PreviousColliderTransform::default(), + )); + } + } + for child in children.iter_descendants(entity) { + if let Ok(collider_parent) = child_colliders.get_mut(child) { + if let Some(mut collider_parent) = collider_parent { + collider_parent.0 = entity; + } else { + commands.entity(child).insert(( + ColliderParent(entity), + // Todo: This probably causes a one frame delay. Compute real value? + ColliderTransform::default(), + PreviousColliderTransform::default(), + )); + } + } + } + } +} + +/// Updates colliders when the rigid bodies they were attached to have been removed. +fn handle_rigid_body_removals( + mut commands: Commands, + colliders: Query<(Entity, &ColliderParent), Without>, + bodies: Query<(), With>, + removals: RemovedComponents, +) { + // Return if no rigid bodies have been removed + if removals.is_empty() { + return; + } + + for (collider_entity, collider_parent) in &colliders { + // If the body associated with the collider parent entity doesn't exist, + // remove ColliderParent and ColliderTransform. + if !bodies.contains(collider_parent.get()) { + commands.entity(collider_entity).remove::<( + ColliderParent, + ColliderTransform, + PreviousColliderTransform, + )>(); + } + } +} + +#[allow(clippy::type_complexity)] +pub(crate) fn update_child_collider_position( + mut colliders: Query< + ( + &ColliderTransform, + &mut Position, + &mut Rotation, + &ColliderParent, + ), + Without, + >, + parents: Query<(&Position, &Rotation), (With, With)>, +) { + for (collider_transform, mut position, mut rotation, parent) in &mut colliders { + let Ok((parent_pos, parent_rot)) = parents.get(parent.get()) else { + continue; + }; + + position.0 = parent_pos.0 + parent_rot.rotate(collider_transform.translation); + #[cfg(feature = "2d")] + { + *rotation = *parent_rot + collider_transform.rotation; + } + #[cfg(feature = "3d")] + { + *rotation = (parent_rot.0 * collider_transform.rotation.0) + .normalize() + .into(); + } + } +} + +/// Updates the scale of colliders based on [`Transform`] scale. +#[allow(clippy::type_complexity)] +pub fn update_collider_scale( + mut colliders: ParamSet<( + // Root bodies + Query<(&Transform, &mut C), Without>, + // Child colliders + Query<(&ColliderTransform, &mut C), With>, + )>, +) { + // Update collider scale for root bodies + for (transform, mut collider) in &mut colliders.p0() { + #[cfg(feature = "2d")] + let scale = transform.scale.truncate().adjust_precision(); + #[cfg(feature = "3d")] + let scale = transform.scale.adjust_precision(); + if scale != collider.scale() { + // TODO: Support configurable subdivision count for shapes that + // can't be represented without approximations after scaling. + collider.set_scale(scale, 10); + } + } + + // Update collider scale for child colliders + for (collider_transform, mut collider) in &mut colliders.p1() { + if collider_transform.scale != collider.scale() { + // TODO: Support configurable subdivision count for shapes that + // can't be represented without approximations after scaling. + collider.set_scale(collider_transform.scale, 10); + } + } +} + +// `ColliderTransform` propagation should only be continued if the child +// is a collider (has a `ColliderTransform`) or is a `ColliderAncestor`. +type ShouldPropagate = Or<(With, With)>; + +/// Updates [`ColliderTransform`]s based on entity hierarchies. Each transform is computed by recursively +/// traversing the children of each rigid body and adding their transforms together to form +/// the total transform relative to the body. +/// +/// This is largely a clone of `propagate_transforms` in `bevy_transform`. +#[allow(clippy::type_complexity)] +pub(crate) fn propagate_collider_transforms( + mut root_query: Query< + (Entity, Ref, &Children), + (Without, With), + >, + collider_query: Query< + ( + Ref, + Option<&mut ColliderTransform>, + Option<&Children>, + ), + (With, ShouldPropagate), + >, + parent_query: Query<(Entity, Ref, Has, Ref), ShouldPropagate>, +) { + root_query.par_iter_mut().for_each( + |(entity, transform,children)| { + for (child, child_transform, is_child_rb, parent) in parent_query.iter_many(children) { + assert_eq!( + parent.get(), entity, + "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" + ); + let child_transform = ColliderTransform::from(*child_transform); + + // SAFETY: + // - `child` must have consistent parentage, or the above assertion would panic. + // Since `child` is parented to a root entity, the entire hierarchy leading to it is consistent. + // - We may operate as if all descendants are consistent, since `propagate_collider_transform_recursive` will panic before + // continuing to propagate if it encounters an entity with inconsistent parentage. + // - Since each root entity is unique and the hierarchy is consistent and forest-like, + // other root entities' `propagate_collider_transform_recursive` calls will not conflict with this one. + // - Since this is the only place where `collider_query` gets used, there will be no conflicting fetches elsewhere. + unsafe { + propagate_collider_transforms_recursive( + if is_child_rb { + ColliderTransform { + scale: child_transform.scale, + ..default() + } + } else { + let transform = ColliderTransform::from(*transform); + + ColliderTransform { + translation: transform.scale * child_transform.translation, + rotation: child_transform.rotation, + scale: (transform.scale * child_transform.scale).max(Vector::splat(Scalar::EPSILON)), + } + }, + &collider_query, + &parent_query, + child, + transform.is_changed() || parent.is_changed() + ); + } + } + }, + ); +} + +/// Recursively computes the [`ColliderTransform`] for `entity` and all of its descendants +/// by propagating transforms. +/// +/// This is largely a clone of `propagate_recursive` in `bevy_transform`. +/// +/// # Panics +/// +/// If `entity`'s descendants have a malformed hierarchy, this function will panic occur before propagating +/// the transforms of any malformed entities and their descendants. +/// +/// # Safety +/// +/// - While this function is running, `collider_query` must not have any fetches for `entity`, +/// nor any of its descendants. +/// - The caller must ensure that the hierarchy leading to `entity` +/// is well-formed and must remain as a tree or a forest. Each entity must have at most one parent. +#[allow(clippy::type_complexity)] +unsafe fn propagate_collider_transforms_recursive( + transform: ColliderTransform, + collider_query: &Query< + ( + Ref, + Option<&mut ColliderTransform>, + Option<&Children>, + ), + (With, ShouldPropagate), + >, + parent_query: &Query<(Entity, Ref, Has, Ref), ShouldPropagate>, + entity: Entity, + mut changed: bool, +) { + let children = { + // SAFETY: This call cannot create aliased mutable references. + // - The top level iteration parallelizes on the roots of the hierarchy. + // - The caller ensures that each child has one and only one unique parent throughout the entire + // hierarchy. + // + // For example, consider the following malformed hierarchy: + // + // A + // / \ + // B C + // \ / + // D + // + // D has two parents, B and C. If the propagation passes through C, but the Parent component on D points to B, + // the above check will panic as the origin parent does match the recorded parent. + // + // Also consider the following case, where A and B are roots: + // + // A B + // \ / + // C D + // \ / + // E + // + // Even if these A and B start two separate tasks running in parallel, one of them will panic before attempting + // to mutably access E. + let Ok((transform_ref, collider_transform, children)) = + (unsafe { collider_query.get_unchecked(entity) }) + else { + return; + }; + + changed |= transform_ref.is_changed(); + if changed { + if let Some(mut collider_transform) = collider_transform { + if *collider_transform != transform { + *collider_transform = transform; + } + } + } + + children + }; + + let Some(children) = children else { return }; + for (child, child_transform, is_rb, parent) in parent_query.iter_many(children) { + assert_eq!( + parent.get(), entity, + "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" + ); + + let child_transform = ColliderTransform::from(*child_transform); + + // SAFETY: The caller guarantees that `collider_query` will not be fetched + // for any descendants of `entity`, so it is safe to call `propagate_collider_transforms_recursive` for each child. + // + // The above assertion ensures that each child has one and only one unique parent throughout the + // entire hierarchy. + unsafe { + propagate_collider_transforms_recursive( + if is_rb { + ColliderTransform { + scale: child_transform.scale, + ..default() + } + } else { + ColliderTransform { + translation: transform.transform_point(child_transform.translation), + #[cfg(feature = "2d")] + rotation: transform.rotation + child_transform.rotation, + #[cfg(feature = "3d")] + rotation: Rotation(transform.rotation.0 * child_transform.rotation.0), + scale: (transform.scale * child_transform.scale) + .max(Vector::splat(Scalar::EPSILON)), + } + }, + collider_query, + parent_query, + child, + changed || parent.is_changed(), + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn collider_ancestor_markers() { + let mut app = App::new(); + + app.init_schedule(PhysicsSchedule); + app.init_schedule(SubstepSchedule); + + app.add_plugins(ColliderHierarchyPlugin::::new(PostUpdate)); + + let collider = Collider::capsule(2.0, 0.5); + + // Set up an entity tree like the following: + // + // AN + // / \ + // BN CY + // / \ + // DN EN + // / \ + // FY GY + // + // where Y means that the entity has a collider, + // and N means that the entity does not have a collider. + + let an = app.world_mut().spawn_empty().id(); + + let bn = app.world_mut().spawn_empty().set_parent(an).id(); + let cy = app.world_mut().spawn(collider.clone()).set_parent(an).id(); + + let dn = app.world_mut().spawn_empty().set_parent(cy).id(); + let en = app.world_mut().spawn_empty().set_parent(cy).id(); + + let fy = app.world_mut().spawn(collider.clone()).set_parent(dn).id(); + let gy = app.world_mut().spawn(collider.clone()).set_parent(dn).id(); + + app.world_mut().run_schedule(PostUpdate); + + // Check that the correct entities have the `ColliderAncestor` component. + assert!(app.world().entity(an).contains::()); + assert!(!app.world().entity(bn).contains::()); + assert!(app.world().entity(cy).contains::()); + assert!(app.world().entity(dn).contains::()); + assert!(!app.world().entity(en).contains::()); + assert!(!app.world().entity(fy).contains::()); + assert!(!app.world().entity(gy).contains::()); + + // Remove the collider from FY. DN, CY, and AN should all keep the `ColliderAncestor` marker. + let mut entity_mut = app.world_mut().entity_mut(fy); + entity_mut.remove::(); + + app.world_mut().run_schedule(PostUpdate); + + assert!(app.world().entity(dn).contains::()); + assert!(app.world().entity(cy).contains::()); + assert!(app.world().entity(an).contains::()); + + // Remove the collider from GY. The `ColliderAncestor` marker should + // now be removed from DN and CY, but it should remain on AN. + let mut entity_mut = app.world_mut().entity_mut(gy); + entity_mut.remove::(); + + app.world_mut().run_schedule(PostUpdate); + + assert!(!app.world().entity(dn).contains::()); + assert!(!app.world().entity(cy).contains::()); + assert!(app.world().entity(an).contains::()); + + // Remove the collider from CY. The `ColliderAncestor` marker should + // now be removed from AN. + let mut entity_mut = app.world_mut().entity_mut(cy); + entity_mut.remove::(); + + app.world_mut().run_schedule(PostUpdate); + + assert!(!app.world().entity(an).contains::()); + } +} diff --git a/src/plugins/collision/collider/mod.rs b/src/plugins/collision/collider/mod.rs index 561ed73b..11ddf921 100644 --- a/src/plugins/collision/collider/mod.rs +++ b/src/plugins/collision/collider/mod.rs @@ -1,3 +1,5 @@ +//! Components, traits, and plugins related to collider functionality. + use crate::prelude::*; #[cfg(all(feature = "3d", feature = "async-collider"))] use bevy::utils::HashMap; @@ -7,6 +9,13 @@ use bevy::{ utils::HashSet, }; +mod backend; +mod hierarchy; + +pub use backend::ColliderBackendPlugin; +pub use hierarchy::ColliderHierarchyPlugin; +pub(crate) use hierarchy::PreviousColliderTransform; + /// The default [`Collider`] that uses Parry. #[cfg(all( feature = "default-collider", diff --git a/src/plugins/collision/mod.rs b/src/plugins/collision/mod.rs index 91271e4c..a0dce92c 100644 --- a/src/plugins/collision/mod.rs +++ b/src/plugins/collision/mod.rs @@ -10,11 +10,10 @@ //! //! You can also find several utility methods for computing contacts in [`contact_query`]. -mod collider; +pub mod collider; pub use collider::*; pub mod broad_phase; -pub mod collider_backend; #[cfg(all( feature = "default-collider", any(feature = "parry-f32", feature = "parry-f64") diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index da8a2aed..5ef08024 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -32,7 +32,9 @@ pub mod spatial_query; pub mod sync; pub use collision::{ - broad_phase::BroadPhasePlugin, collider_backend::*, contact_reporting::ContactReportingPlugin, + broad_phase::BroadPhasePlugin, + collider::{ColliderBackendPlugin, ColliderHierarchyPlugin}, + contact_reporting::ContactReportingPlugin, narrow_phase::NarrowPhasePlugin, }; #[cfg(feature = "debug-plugin")] @@ -59,6 +61,7 @@ use bevy::prelude::*; /// and [colliders](Collider) and updates components. /// - [`ColliderBackendPlugin`]: Handles generic collider backend logic, like initializing colliders and AABBs /// and updating related components. +/// - [`ColliderHierarchyPlugin`]: Handles transform propagation, scale updates, and [`ColliderParent`] updates for colliders. /// - [`BroadPhasePlugin`]: Collects pairs of potentially colliding entities into [`BroadCollisionPairs`] using /// [AABB](ColliderAabb) intersection checks. /// - [`NarrowPhasePlugin`]: Computes contacts between entities and sends collision events. @@ -198,6 +201,7 @@ impl PluginGroup for PhysicsPlugins { ))] let builder = builder .add(ColliderBackendPlugin::::new(self.schedule)) + .add(ColliderHierarchyPlugin::::new(self.schedule)) .add(NarrowPhasePlugin::::default()); builder