diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a0b69042..9b24635c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,10 +52,10 @@ jobs: run: cargo clippy --verbose -p bevy_rapier2d --examples - name: Clippy for bevy_rapier3d run: cargo clippy --verbose -p bevy_rapier3d --examples - - name: Clippy for bevy_rapier2d (debug-render, simd, serde) - run: cargo clippy --verbose -p bevy_rapier2d --features debug-render-2d,simd-stable,serde-serialize,picking-backend - - name: Clippy for bevy_rapier3d (debug-render, simd, serde) - run: cargo clippy --verbose -p bevy_rapier3d --features debug-render-3d,simd-stable,serde-serialize,picking-backend + - name: Clippy for bevy_rapier2d (debug-render, simd8, serde) + run: cargo clippy --verbose -p bevy_rapier2d --features debug-render-2d,simd8,serde-serialize,picking-backend + - name: Clippy for bevy_rapier3d (debug-render, simd8, serde) + run: cargo clippy --verbose -p bevy_rapier3d --features debug-render-3d,simd8,serde-serialize,picking-backend - name: Test for bevy_rapier2d run: cargo test --verbose -p bevy_rapier2d - name: Test for bevy_rapier3d diff --git a/CHANGELOG.md b/CHANGELOG.md index 33cdcb36..bf4b8937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## Unreleased + +### Modified + +- Update to bevy `0.20` (currently tracking bevy’s `main` branch). +- Update from rapier `0.34` to rapier `0.35`. + See [rapier's changelog](https://github.com/dimforge/rapier/blob/master/CHANGELOG.md) for details. + - Removed the `simd-stable` and `simd-nightly` features: SIMD is now always enabled in rapier. + The new `simd8` feature widens the solver’s SIMD from 4 to 8 lanes (f32 only, needs an AVX2-capable + target). Just drop `simd-stable`/`simd-nightly` from your `Cargo.toml`. + - `SolverContactView::point` is now derived from the contact’s per-body anchors, and + `SolverContactView::{friction, restitution}` report the value of the contact’s manifold, since + rapier made both coefficients per-manifold instead of per-contact. + - The `enhanced-determinism` and `serde-serialize` features can’t currently be enabled together: + `enhanced-determinism` turns on `glam/scalar-math`, for which glam doesn’t implement `serde` on + `BVec3A`/`BVec4A`, which `bevy_reflect` requires when bevy’s `serialize` feature is on. + ## v0.35.0 (12 July 2026) ### Modified diff --git a/Cargo.toml b/Cargo.toml index ffde69f8..3f2c891c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,8 @@ members = ["bevy_rapier2d", "bevy_rapier3d", "bevy_rapier_benches3d", "ci"] resolver = "2" [workspace.dependencies] -rapier2d = "=0.33.0-alpha" -rapier3d = "=0.33.0-alpha" +rapier2d = "0.35.0" +rapier3d = "0.35.0" [profile.dev] # Use slightly better optimization by default, as examples otherwise seem laggy. @@ -14,13 +14,6 @@ opt-level = 1 codegen-units = 1 [patch.crates-io] -#nalgebra = { path = "../nalgebra" } -#parry2d = { path = "../parry/crates/parry2d" } -#parry3d = { path = "../parry/crates/parry3d" } #rapier2d = { path = "../rapier/crates/rapier2d" } #rapier3d = { path = "../rapier/crates/rapier3d" } -#nalgebra = { git = "https://github.com/dimforge/nalgebra", branch = "dev" } -#parry2d = { git = "https://github.com/dimforge/parry", branch = "master" } -#parry3d = { git = "https://github.com/dimforge/parry", branch = "master" } -#rapier2d = { git = "https://github.com/dimforge/rapier", branch = "character-controller" } -#rapier3d = { git = "https://github.com/dimforge/rapier", branch = "character-controller" } +bevy = { git = "https://github.com/bevyengine/bevy", rev = "87a24bf95af15ed707fe634969994852155a2eac" } diff --git a/bevy_rapier2d/Cargo.toml b/bevy_rapier2d/Cargo.toml index 16f18aa6..6e12a7e6 100644 --- a/bevy_rapier2d/Cargo.toml +++ b/bevy_rapier2d/Cargo.toml @@ -49,11 +49,12 @@ debug-render-3d = [ rapier-debug-render = ["rapier2d/debug-render"] parallel = ["rapier2d/parallel"] -simd-stable = ["rapier2d/simd-stable"] -simd-nightly = ["rapier2d/simd-nightly"] +simd8 = ["rapier2d/simd8"] serde-serialize = ["rapier2d/serde-serialize", "bevy/serialize", "serde"] enhanced-determinism = ["rapier2d/enhanced-determinism"] headless = [] +# Depends on bevy_egui / bevy-inspector-egui / bevy_mod_debugdump which don't support bevy main yet. +egui-examples = [] picking-backend = ["bevy/bevy_picking", "bevy/bevy_render"] async-collider = [ "bevy/bevy_asset", @@ -64,15 +65,15 @@ async-collider = [ to-bevy-mesh = ["bevy/bevy_render", "bevy/bevy_asset"] [dependencies] -bevy = { version = "0.19.0", default-features = false, features = ["std"] } -nalgebra = { version = "0.34.2", features = ["convert-glam032"] } +bevy = { version = "0.20.0-dev", default-features = false, features = ["std"] } +nalgebra = { version = "0.35", features = ["convert-glam033"] } rapier2d = { workspace = true } bitflags = "2.10.0" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] -bevy = { version = "0.19.0", default-features = false, features = [ +bevy = { version = "0.20.0-dev", default-features = false, features = [ "x11", "bevy_state", "bevy_window", @@ -92,10 +93,10 @@ bevy = { version = "0.19.0", default-features = false, features = [ ] } oorandom = "11" approx = "0.5.1" -glam = { version = "0.32.1", features = ["approx"] } -bevy-inspector-egui = "0.37.0" -bevy_egui = "0.40.0" -bevy_mod_debugdump = "0.16.0" +glam = { version = "0.33", features = ["approx"] } +#bevy-inspector-egui = "0.37.0" # FIXME: re-enable once it supports bevy main +#bevy_egui = "0.40.0" # FIXME: re-enable once it supports bevy main +#bevy_mod_debugdump = "0.16.0" # FIXME: re-enable once it supports bevy main serde_json = "1.0" [package.metadata.docs.rs] @@ -104,10 +105,14 @@ features = ["debug-render-2d", "serde-serialize"] [package.metadata.cargo-all-features] -denylist = ["simd-nightly"] - -# Features "dim2" and "dim3" are incompatible, so skip permutations including them -skip_feature_sets = [["enhanced-determinism", "simd-stable"]] +# Features "dim2" and "dim3" are incompatible, so skip permutations including them. +# `enhanced-determinism` + `serde-serialize` is skipped because the former enables +# `glam/scalar-math`, for which glam doesn’t implement `serde` on `BVec3A`/`BVec4A`, +# which `bevy_reflect` needs when bevy’s `serialize` feature is on. +skip_feature_sets = [ + ["enhanced-determinism", "simd8"], + ["enhanced-determinism", "serde-serialize"], +] always_include_features = ["dim2"] @@ -115,3 +120,11 @@ always_include_features = ["dim2"] # This is useful for reducing the number of combinations run for a crate with a large amount of features, # since in most cases a bug just needs a small set of 2-3 features to reproduce. max_combination_size = 1 + +[[example]] +name = "testbed2" +required-features = ["egui-examples"] + +[[example]] +name = "debugdump2" +required-features = ["egui-examples"] diff --git a/bevy_rapier3d/Cargo.toml b/bevy_rapier3d/Cargo.toml index 39bbe7e7..a76eddec 100644 --- a/bevy_rapier3d/Cargo.toml +++ b/bevy_rapier3d/Cargo.toml @@ -50,11 +50,12 @@ debug-render-3d = [ rapier-debug-render = ["rapier3d/debug-render"] parallel = ["rapier3d/parallel"] -simd-stable = ["rapier3d/simd-stable"] -simd-nightly = ["rapier3d/simd-nightly"] +simd8 = ["rapier3d/simd8"] serde-serialize = ["rapier3d/serde-serialize", "bevy/serialize", "serde"] enhanced-determinism = ["rapier3d/enhanced-determinism"] headless = [] +# Depends on bevy_egui / bevy-inspector-egui / bevy_mod_debugdump which don't support bevy main yet. +egui-examples = [] picking-backend = ["bevy/bevy_picking", "bevy/bevy_render"] async-collider = [ "bevy/bevy_asset", @@ -65,18 +66,19 @@ async-collider = [ to-bevy-mesh = ["bevy/bevy_render", "bevy/bevy_asset"] [dependencies] -bevy = { version = "0.19.0", default-features = false, features = ["std"] } -nalgebra = { version = "0.34.2", features = ["convert-glam032"] } +bevy = { version = "0.20.0-dev", default-features = false, features = ["std"] } +nalgebra = { version = "0.35", features = ["convert-glam033"] } rapier3d = { workspace = true } bitflags = "2.10.0" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] -bevy = { version = "0.19.0", default-features = false, features = [ +bevy = { version = "0.20.0-dev", default-features = false, features = [ "bevy_window", "x11", "tonemapping_luts", + "zstd_rust", "bevy_state", "bevy_debug_stepping", "bevy_text", @@ -86,10 +88,10 @@ bevy = { version = "0.19.0", default-features = false, features = [ "bevy_gizmos_render", ] } approx = "0.5.1" -glam = { version = "0.32.1", features = ["approx"] } -bevy-inspector-egui = "0.37.0" -bevy_egui = "0.40.0" -bevy_mod_debugdump = "0.16.0" +glam = { version = "0.33", features = ["approx"] } +#bevy-inspector-egui = "0.37.0" # FIXME: re-enable once it supports bevy main +#bevy_egui = "0.40.0" # FIXME: re-enable once it supports bevy main +#bevy_mod_debugdump = "0.16.0" # FIXME: re-enable once it supports bevy main [package.metadata.docs.rs] # Enable all the features when building the docs on docs.rs @@ -97,10 +99,14 @@ features = ["debug-render-3d", "serde-serialize", "async-collider"] [package.metadata.cargo-all-features] -denylist = ["simd-nightly"] - -# Features "dim2" and "dim3" are incompatible, so skip permutations including them -skip_feature_sets = [["enhanced-determinism", "simd-stable"]] +# Features "dim2" and "dim3" are incompatible, so skip permutations including them. +# `enhanced-determinism` + `serde-serialize` is skipped because the former enables +# `glam/scalar-math`, for which glam doesn’t implement `serde` on `BVec3A`/`BVec4A`, +# which `bevy_reflect` needs when bevy’s `serialize` feature is on. +skip_feature_sets = [ + ["enhanced-determinism", "simd8"], + ["enhanced-determinism", "serde-serialize"], +] always_include_features = ["dim3"] @@ -115,4 +121,8 @@ required-features = ["picking-backend"] [[example]] name = "testbed3" -required-features = ["picking-backend"] +required-features = ["picking-backend", "egui-examples"] + +[[example]] +name = "debugdump3" +required-features = ["egui-examples"] diff --git a/bevy_rapier_benches3d/Cargo.toml b/bevy_rapier_benches3d/Cargo.toml index a6e8661b..0c33e203 100644 --- a/bevy_rapier_benches3d/Cargo.toml +++ b/bevy_rapier_benches3d/Cargo.toml @@ -5,13 +5,14 @@ description = "Custom benchmarks for bevy_rapier." readme = "./README.md" license = "Apache-2.0" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rapier3d = { workspace = true } +rapier3d = { workspace = true, features = ["profiler"] } bevy_rapier3d = { version = "0.35", path = "../bevy_rapier3d" } -bevy = { version = "0.19.0", default-features = false } +bevy = { version = "0.20.0-dev", default-features = false } [dev-dependencies] divan = "0.1" diff --git a/ci/Cargo.toml b/ci/Cargo.toml index 41a600c5..64951bbf 100644 --- a/ci/Cargo.toml +++ b/ci/Cargo.toml @@ -2,9 +2,10 @@ name = "ci" version = "0.1.0" edition = "2021" +publish = false [dependencies] -bevy = "0.19.0" +bevy = { version = "0.20.0-dev" } bevy_rapier3d = { version = "0.35", path = "../bevy_rapier3d" } bevy_rapier2d = { version = "0.35", path = "../bevy_rapier2d" } diff --git a/publish.sh b/publish.sh index c9413ca8..b1422508 100755 --- a/publish.sh +++ b/publish.sh @@ -1,25 +1,87 @@ -#!/bin/sh -set -e +#! /bin/bash +# +# Publishes both bevy_rapier crates to crates.io with a single +# `cargo publish --workspace` invocation: +# +# - bevy_rapier2d / bevy_rapier3d +# +# Cargo computes the dependency order itself and waits for each crate to become +# available on the registry before publishing the ones that depend on it. The +# `bevy_rapier_benches3d` and `ci` crates are marked `publish = false`, so +# `--workspace` skips them. +# +# Why this script exists +# ---------------------- +# Both crates share a single source tree at `/src`, referenced from each +# manifest as `path = "../src/lib.rs"`. That path points outside the crate +# directory, which `cargo publish` refuses to package. +# +# To work around it *only during publishing*, this script temporarily, for each +# affected crate: +# 1. rewrites the `[lib] path` to a crate-local one (i.e. `src/lib.rs`), and +# 2. creates a symlink inside the crate pointing at the shared source tree. +# +# Cargo follows the symlink and bundles the real source into each `.crate`. A +# trap restores the manifests and removes the symlinks on exit (including on +# error or Ctrl-C), leaving the tree exactly as it was. +# +# Extra arguments are forwarded to `cargo publish`, e.g.: +# ./publish.sh --dry-run +# ./publish.sh --token "$CARGO_TOKEN" +# +# Requires cargo >= 1.90 (for `cargo publish --workspace`). -tmp=$(mktemp -d) +set -euo pipefail -echo "$tmp" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" -cp -r src "$tmp"/. -cp -r LICENSE README.md "$tmp"/. +# Crates sharing `src/lib.rs`. +SRC_CRATES=(bevy_rapier2d bevy_rapier3d) -### Publish the 2D version. -sed 's#\.\./src#src#g' bevy_rapier2d/Cargo.toml > "$tmp"/Cargo.toml -gawk -i inplace '{ gsub("../README.md", "README.md") }; { print }' "$tmp"/Cargo.toml -currdir=$(pwd) -cd "$tmp" && cargo publish -cd "$currdir" || exit +# Refuse to run on a dirty tree: the only diff during publishing must be our own +# temporary edits, so the restore at the end is guaranteed to be correct. +if [ -n "$(git status --porcelain)" ]; then + echo "error: working tree is not clean. Commit or stash changes before publishing." >&2 + exit 1 +fi +backup_dir="$(mktemp -d)" -### Publish the 3D version. -sed 's#\.\./src#src#g' bevy_rapier3d/Cargo.toml > "$tmp"/Cargo.toml -gawk -i inplace '{ gsub("../README.md", "README.md") }; { print }' "$tmp"/Cargo.toml -cp -r LICENSE README.md "$tmp"/. -cd "$tmp" && cargo publish +cleanup() { + for crate in "${SRC_CRATES[@]}"; do + # Remove the symlink we created (only if it is in fact a symlink). + [ -L "$crate/src" ] && rm -f "$crate/src" + # Restore the original manifest. + if [ -f "$backup_dir/$crate.Cargo.toml" ]; then + cp "$backup_dir/$crate.Cargo.toml" "$crate/Cargo.toml" + fi + done + rm -rf "$backup_dir" +} +trap cleanup EXIT INT TERM -rm -rf "$tmp" +# Apply the temporary symlink layout: the shared `src` dir lives at the repo +# root and is linked into each crate, while the manifest's `[lib] path` is made +# crate-local. +link_shared() { + local crate="$1" + local manifest="$crate/Cargo.toml" + + cp "$manifest" "$backup_dir/$crate.Cargo.toml" + + local tmp + tmp="$(mktemp)" + sed 's#path = "\.\./src/lib.rs"#path = "src/lib.rs"#' "$manifest" > "$tmp" + mv "$tmp" "$manifest" + + ln -s ../src "$crate/src" +} + +for crate in "${SRC_CRATES[@]}"; do + link_shared "$crate" +done + +# Publish the whole workspace. `--allow-dirty` is required because our temporary +# edits make the tree dirty; the clean-tree check above keeps that safe. +cargo publish --workspace --allow-dirty "$@" diff --git a/src/plugin/context/mod.rs b/src/plugin/context/mod.rs index 69dc4949..a9c625fe 100644 --- a/src/plugin/context/mod.rs +++ b/src/plugin/context/mod.rs @@ -432,16 +432,16 @@ impl<'a> RapierQueryPipeline<'a> { /// /// # Parameters /// * `point` - The point to project. - /// * `solid` - If this is set to `true` then the collider shapes are considered to - /// be plain (if the point is located inside of a plain shape, its projection is the point - /// itself). If it is set to `false` the collider shapes are considered to be hollow - /// (if the point is located inside of an hollow shape, it is projected on the shape's - /// boundary). + /// * `max_dist` - Colliders further away than this distance from the point are ignored. + /// Pass `Real::MAX` to search among all the colliders. pub fn project_point_and_get_feature( &self, point: Vect, + max_dist: Real, ) -> Option<(Entity, PointProjection, FeatureId)> { - let (h, proj, fid) = self.query_pipeline.project_point_and_get_feature(point)?; + let (h, proj, fid) = self + .query_pipeline + .project_point_and_get_feature(point, max_dist)?; Some(( self.collider_entity(h), @@ -622,8 +622,8 @@ impl RapierRigidBodySet { let impulse_joint = joints.impulse_joints.get(*joint_handle)?; let revolute_joint = impulse_joint.data.as_revolute()?; - let rb1 = &self.bodies[impulse_joint.body1]; - let rb2 = &self.bodies[impulse_joint.body2]; + let rb1 = &self.bodies[impulse_joint.body1()]; + let rb2 = &self.bodies[impulse_joint.body2()]; Some(revolute_joint.angle(rb1.rotation(), rb2.rotation())) } } @@ -658,7 +658,8 @@ pub struct RapierContextSimulation { /// The integration parameters, controlling various low-level coefficient of the simulation. pub integration_parameters: IntegrationParameters, #[cfg_attr(feature = "serde-serialize", serde(skip))] - pub(crate) event_handler: Option>, + // NOTE: rapier only requires `EventHandler: Sync`, but bevy components must be `Send` too. + pub(crate) event_handler: Option>, // This maps the handles of colliders that have been deleted since the last // physics update, to the entity they was attached to. /// NOTE: this map is needed to handle despawning. @@ -725,6 +726,7 @@ impl RapierContextSimulation { let event_handler = self .event_handler .as_deref() + .map(|handler| handler as &dyn EventHandler) .or_else(|| event_queue.as_ref().map(|q| q as &dyn EventHandler)) .unwrap_or(&() as &dyn EventHandler); diff --git a/src/plugin/narrow_phase.rs b/src/plugin/narrow_phase.rs index 4870360c..d84d5210 100644 --- a/src/plugin/narrow_phase.rs +++ b/src/plugin/narrow_phase.rs @@ -1,7 +1,9 @@ use crate::math::{Real, Vect}; use crate::plugin::context::{RapierContextColliders, RapierContextSimulation, RapierRigidBodySet}; use bevy::prelude::*; -use rapier::geometry::{Contact, ContactManifold, ContactPair, SolverContact, SolverFlags}; +use rapier::geometry::{ + Contact, ContactManifold, ContactPair, SolverContact, SolverFlags, NEW_CONTACT_BIT, +}; impl RapierContextSimulation { /// All the contact pairs involving the non-sensor collider attached to the given entity. @@ -214,7 +216,11 @@ impl ContactManifoldView<'_> { .data .solver_contacts .get(i) - .map(|raw| SolverContactView { raw }) + .map(|raw| SolverContactView { + raw, + manifold: self.raw, + rigidbody_set: self.rigidbody_set, + }) } /// The contacts that will be seen by the constraints solver for computing forces. @@ -223,7 +229,11 @@ impl ContactManifoldView<'_> { .data .solver_contacts .iter() - .map(|raw| SolverContactView { raw }) + .map(|raw| SolverContactView { + raw, + manifold: self.raw, + rigidbody_set: self.rigidbody_set, + }) } /// The relative dominance of the bodies involved in this contact manifold. @@ -304,25 +314,32 @@ impl ContactView<'_> { pub struct SolverContactView<'a> { /// The raw solver contact from Rapier. pub raw: &'a SolverContact, + /// The contact manifold this solver contact is part of. + pub manifold: &'a ContactManifold, + rigidbody_set: &'a RapierRigidBodySet, } impl SolverContactView<'_> { /// The world-space contact point. pub fn point(&self) -> Vect { - self.raw.point + let (p1, p2) = self + .manifold + .data + .solver_contact_world_points(self.raw, &self.rigidbody_set.bodies); + (p1 + p2) / 2.0 } /// The distance between the two original contacts points along the contact normal. /// If negative, this is measures the penetration depth. pub fn dist(&self) -> Real { self.raw.dist } - /// The effective friction coefficient at this contact point. + /// The effective friction coefficient of this contact's manifold. pub fn friction(&self) -> Real { - self.raw.friction + self.manifold.data.friction } - /// The effective restitution coefficient at this contact point. + /// The effective restitution coefficient of this contact's manifold. pub fn restitution(&self) -> Real { - self.raw.restitution + self.manifold.data.restitution } /// The desired tangent relative velocity at the contact point. /// @@ -333,7 +350,7 @@ impl SolverContactView<'_> { } /// Whether or not this contact existed during the last timestep. pub fn is_new(&self) -> bool { - self.raw.is_new == 1.0 + self.raw.contact_id[0] & NEW_CONTACT_BIT != 0 } } diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 3c19cc75..1c616148 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -538,7 +538,7 @@ mod test { .single(app.world()) .unwrap(); - println!("{:?}", &rigidbody_set.entity2body); + println!("{:?}", rigidbody_set.entity2body); } let rigidbody_set = app .world_mut() @@ -624,7 +624,7 @@ mod test { let mut context_query = app.world_mut().query::(); let context = context_query.single(app.world()).unwrap(); - println!("{:?}", &context.rigidbody_set.entity2body); + println!("{:?}", context.rigidbody_set.entity2body); assert_eq!(context.rigidbody_set.entity2body.len(), 0); } @@ -694,7 +694,7 @@ mod test { let mut context_query = app.world_mut().query::(); let context = context_query.single(app.world()).unwrap(); - println!("{:#?}", &context.rigidbody_set.bodies); + println!("{:#?}", context.rigidbody_set.bodies); } pub fn init_rapier_configuration( diff --git a/src/reflect/mod.rs b/src/reflect/mod.rs index 1db98e64..52eb9cbd 100644 --- a/src/reflect/mod.rs +++ b/src/reflect/mod.rs @@ -66,6 +66,14 @@ pub struct IntegrationParametersWrapper { #[reflect(remote = SpringCoefficientsWrapper)] pub contact_softness: SpringCoefficients, + /// Softness coefficients for contact constraints where one side is a fixed body. + /// + /// Stiffer than [`IntegrationParameters::contact_softness`] by default so bodies are + /// held firmly against static walls/floors; set equal to + /// [`IntegrationParameters::contact_softness`] to disable. + #[reflect(remote = SpringCoefficientsWrapper)] + pub static_contact_softness: SpringCoefficients, + /// The coefficient in `[0, 1]` applied to warmstart impulses, i.e., impulses that are used as the /// initial solution (instead of 0) at the next simulation step. /// @@ -101,16 +109,44 @@ pub struct IntegrationParametersWrapper { /// /// This value is implicitly scaled by [`IntegrationParameters::length_unit`]. pub normalized_prediction_distance: Real, + /// Maximum linear velocity a body may have after each solver substep (default: `400.0` m/s). + /// + /// Bounding per-step travel keeps CCD and speculative contacts reliable (a body cannot be + /// flung or crushed to an arbitrary speed); set to `Real::MAX` to disable. + /// + /// This value is implicitly scaled by [`IntegrationParameters::length_unit`]. + pub normalized_max_linear_velocity: Real, /// The number of solver iterations run by the constraints solver for calculating forces (default: `4`). pub num_solver_iterations: usize, /// Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). pub num_internal_pgs_iterations: usize, /// The number of stabilization iterations run at each solver iterations (default: `1`). pub num_internal_stabilization_iterations: usize, - /// Minimum number of dynamic bodies in each active island (default: `128`). - pub min_island_size: usize, /// Maximum number of substeps performed by the solver (default: `1`). pub max_ccd_substeps: usize, + /// If enabled, contact manifolds of a collider pair sharing (nearly) the same normal are merged + /// into one "cluster" manifold before constraint generation (default: `true`, 3D only), so at + /// most 4 contact points are solved per contact plane — a large solver win on composite shapes + /// (meshes, heightfields, compounds, voxels) that emit one manifold per subshape. + pub contact_clustering: bool, + /// If enabled, a contact pair whose relative pose moved less than + /// [`IntegrationParameters::normalized_contact_recycle_distance`] since its last full + /// narrow-phase update skips contact determination and keeps its existing points + /// (default: `true`) — a large speed-up for quasi-static scenes. + pub contact_recycling: bool, + /// Maximum relative-pose drift (translation plus rotation-arc) below which a contact pair may + /// be recycled instead of fully updated (default: `0.05`, multiplied by + /// [`IntegrationParameters::length_unit`]). Only used when + /// [`IntegrationParameters::contact_recycling`] is enabled. + pub normalized_contact_recycle_distance: Real, + /// If `false`, friction is only solved during the unbiased (relax) pass of each substep instead + /// of both passes (default: `false`, the "no friction when applying bias" rule). + pub friction_in_bias_pass: bool, + /// If enabled, impulse-joint constraints are warm-started like contacts: impulses accumulated + /// by the previous step are re-applied (scaled by + /// [`IntegrationParameters::warmstart_coefficient`]) at the start of each substep instead of + /// restarting from zero (default: `false`). Multibody joints are unaffected. + pub warmstart_joints: bool, } // These structs are duplicated in their entirety due to [`FrictionModel`] not being available in 2D, and `bevy::reflect_remote` not supporting conditional fields. @@ -137,6 +173,14 @@ pub struct IntegrationParametersWrapper { #[reflect(remote = SpringCoefficientsWrapper)] pub contact_softness: SpringCoefficients, + /// Softness coefficients for contact constraints where one side is a fixed body. + /// + /// Stiffer than [`IntegrationParameters::contact_softness`] by default so bodies are + /// held firmly against static walls/floors; set equal to + /// [`IntegrationParameters::contact_softness`] to disable. + #[reflect(remote = SpringCoefficientsWrapper)] + pub static_contact_softness: SpringCoefficients, + /// The coefficient in `[0, 1]` applied to warmstart impulses, i.e., impulses that are used as the /// initial solution (instead of 0) at the next simulation step. /// @@ -172,16 +216,44 @@ pub struct IntegrationParametersWrapper { /// /// This value is implicitly scaled by [`IntegrationParameters::length_unit`]. pub normalized_prediction_distance: Real, + /// Maximum linear velocity a body may have after each solver substep (default: `400.0` m/s). + /// + /// Bounding per-step travel keeps CCD and speculative contacts reliable (a body cannot be + /// flung or crushed to an arbitrary speed); set to `Real::MAX` to disable. + /// + /// This value is implicitly scaled by [`IntegrationParameters::length_unit`]. + pub normalized_max_linear_velocity: Real, /// The number of solver iterations run by the constraints solver for calculating forces (default: `4`). pub num_solver_iterations: usize, /// Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). pub num_internal_pgs_iterations: usize, /// The number of stabilization iterations run at each solver iterations (default: `1`). pub num_internal_stabilization_iterations: usize, - /// Minimum number of dynamic bodies in each active island (default: `128`). - pub min_island_size: usize, /// Maximum number of substeps performed by the solver (default: `1`). pub max_ccd_substeps: usize, + /// If enabled, contact manifolds of a collider pair sharing (nearly) the same normal are merged + /// into one "cluster" manifold before constraint generation (default: `true`, 3D only), so at + /// most 4 contact points are solved per contact plane — a large solver win on composite shapes + /// (meshes, heightfields, compounds, voxels) that emit one manifold per subshape. + pub contact_clustering: bool, + /// If enabled, a contact pair whose relative pose moved less than + /// [`IntegrationParameters::normalized_contact_recycle_distance`] since its last full + /// narrow-phase update skips contact determination and keeps its existing points + /// (default: `true`) — a large speed-up for quasi-static scenes. + pub contact_recycling: bool, + /// Maximum relative-pose drift (translation plus rotation-arc) below which a contact pair may + /// be recycled instead of fully updated (default: `0.05`, multiplied by + /// [`IntegrationParameters::length_unit`]). Only used when + /// [`IntegrationParameters::contact_recycling`] is enabled. + pub normalized_contact_recycle_distance: Real, + /// If `false`, friction is only solved during the unbiased (relax) pass of each substep instead + /// of both passes (default: `false`, the "no friction when applying bias" rule). + pub friction_in_bias_pass: bool, + /// If enabled, impulse-joint constraints are warm-started like contacts: impulses accumulated + /// by the previous step are re-applied (scaled by + /// [`IntegrationParameters::warmstart_coefficient`]) at the start of each substep instead of + /// restarting from zero (default: `false`). Multibody joints are unaffected. + pub warmstart_joints: bool, /// Friction models used for all contact constraints between two rigid-bodies. #[reflect(remote = FrictionModelWrapper)] pub friction_model: FrictionModel,