From 9f29d767b25f7aea1d1d9129acd7dc834b991912 Mon Sep 17 00:00:00 2001 From: Miguel Angel Charris Carmona Date: Wed, 23 Sep 2026 15:29:36 -0500 Subject: [PATCH 1/4] Say that boxes respond to explosions The remarks on Explode read "Only spheres, capsules and hulls respond." In this API Box and ConvexHull are different types, so that reads as "boxes do not", and a user building a demolition demo built every crate from eight corners with ConvexHull.FromPoints to get around a limitation that does not exist: AddBox makes a hull through b3MakeBoxHull, and a hull is what Box3D pushes. The remarks now say that boxes respond because they are hulls, and why meshes, height fields and compounds do not. b3World_Explode queries only the dynamic-body tree, and those three can only be attached to static bodies; b3GetShapeProjectedArea would also give them no area. The same query is why kinematic bodies are not pushed, which is now stated too. AddBox and ShapeType.Hull say that a box reports ShapeType.Hull, so reading Shape.Type back is not another surprise. ExplosionTests is new and pins each claim: boxes, spheres and capsules are pushed; kinematic bodies and bodies out of reach are not; a sleeping body is woken; and the filter is compared with shape categories. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 11 +++ src/Box3D.NET.Tests/ExplosionTests.cs | 124 ++++++++++++++++++++++++++ src/Box3D.NET/Body.cs | 5 +- src/Box3D.NET/PhysicsWorld.cs | 11 ++- src/Box3D.NET/ShapeDefinition.cs | 4 + 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/Box3D.NET.Tests/ExplosionTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 022b272..857b59b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **`PhysicsWorld.Explode` says which shapes respond.** It read "only spheres, + capsules and hulls respond", which in an API where `Box` and `ConvexHull` are + different types reads as "boxes do not". They do: a box is a hull. The remarks + now say so, and that meshes, height fields and compounds never respond, + because they only go on static bodies and an explosion only reaches dynamic + ones; kinematic bodies are not pushed either. `Body.AddBox` and + `ShapeType.Hull` note that a box reports `ShapeType.Hull`. `ExplosionTests` + pins all of it. + ## [0.5.0] - 2026-09-23 An engine release, and a minor version for two reasons at once: the idiomatic diff --git a/src/Box3D.NET.Tests/ExplosionTests.cs b/src/Box3D.NET.Tests/ExplosionTests.cs new file mode 100644 index 0000000..bd87927 --- /dev/null +++ b/src/Box3D.NET.Tests/ExplosionTests.cs @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT + +using System; +using System.Numerics; +using Xunit; + +namespace Box3D.Tests; + +/// +/// Holds to what its documentation says +/// responds and what does not. +/// +/// +/// The documentation once read "only spheres, capsules and hulls respond", which +/// in an API where and are different +/// types reads as "boxes do not". They do, because a box is a hull, and these +/// tests pin that alongside the cases that genuinely do not respond. +/// +[Collection(NativeCollection.Name)] +public class ExplosionTests : IDisposable +{ + private readonly PhysicsWorld _world = new(WorldSettings.Default with { Gravity = Vector3.Zero }); + + public void Dispose() + { + _world.Dispose(); + GC.SuppressFinalize(this); + } + + // At the default density a one-metre cube weighs a tonne, so the impulse is sized to + // move it at about a metre per second. + private void Blast(ulong mask = ulong.MaxValue) => + _world.Explode(Vector3.Zero, radius: 5.0f, impulsePerArea: 1000.0f, filter: mask); + + [NativeFact] + public void A_box_is_a_hull() + { + Body body = _world.CreateDynamicBody(new Vector3(2.0f, 0.0f, 0.0f)); + Shape box = body.AddBox(Box.Cube(0.5f)); + + Assert.Equal(ShapeType.Hull, box.Type); + } + + [NativeFact] + public void A_box_is_pushed_away_from_the_blast() + { + Body body = _world.CreateDynamicBody(new Vector3(2.0f, 0.0f, 0.0f)); + body.AddBox(Box.Cube(0.5f)); + + Blast(); + + Assert.True(body.LinearVelocity.X > 0.1f, $"the box should be pushed along +x, velocity {body.LinearVelocity}"); + } + + [NativeFact] + public void Spheres_and_capsules_are_pushed_too() + { + Body sphere = _world.CreateDynamicBody(new Vector3(0.0f, 2.0f, 0.0f)); + sphere.AddSphere(new Sphere(0.5f)); + + Body capsule = _world.CreateDynamicBody(new Vector3(0.0f, 0.0f, 2.0f)); + capsule.AddCapsule(Capsule.Upright(1.0f, 0.25f)); + + Blast(); + + Assert.True(sphere.LinearVelocity.Y > 0.1f, $"sphere velocity {sphere.LinearVelocity}"); + Assert.True(capsule.LinearVelocity.Z > 0.1f, $"capsule velocity {capsule.LinearVelocity}"); + } + + [NativeFact] + public void A_kinematic_body_is_not_pushed() + { + Body body = _world.CreateBody(BodyDefinition.Kinematic(new Vector3(2.0f, 0.0f, 0.0f))); + body.AddBox(Box.Cube(0.5f)); + + Blast(); + + Assert.Equal(Vector3.Zero, body.LinearVelocity); + } + + [NativeFact] + public void A_body_beyond_the_radius_and_falloff_is_not_pushed() + { + Body body = _world.CreateDynamicBody(new Vector3(20.0f, 0.0f, 0.0f)); + body.AddBox(Box.Cube(0.5f)); + + Blast(); + + Assert.Equal(Vector3.Zero, body.LinearVelocity); + } + + [NativeFact] + public void A_sleeping_body_is_woken_and_pushed() + { + Body body = _world.CreateDynamicBody(new Vector3(2.0f, 0.0f, 0.0f)); + body.AddBox(Box.Cube(0.5f)); + body.IsAwake = false; + + Blast(); + + Assert.True(body.IsAwake, "the blast should wake the body"); + Assert.True(body.LinearVelocity.X > 0.1f, $"velocity {body.LinearVelocity}"); + } + + [NativeFact] + public void The_filter_is_a_category_mask() + { + var debris = ShapeDefinition.Default with { Filter = CollisionFilter.Default with { Categories = 0x2 } }; + var scenery = ShapeDefinition.Default with { Filter = CollisionFilter.Default with { Categories = 0x4 } }; + + Body affected = _world.CreateDynamicBody(new Vector3(2.0f, 0.0f, 0.0f)); + affected.AddBox(Box.Cube(0.5f), debris); + + Body spared = _world.CreateDynamicBody(new Vector3(-2.0f, 0.0f, 0.0f)); + spared.AddBox(Box.Cube(0.5f), scenery); + + // The mask is compared with each shape's categories, not with what the + // shape collides with. + Blast(mask: 0x2); + + Assert.True(affected.LinearVelocity.X > 0.1f, $"the debris should be pushed, velocity {affected.LinearVelocity}"); + Assert.Equal(Vector3.Zero, spared.LinearVelocity); + } +} diff --git a/src/Box3D.NET/Body.cs b/src/Box3D.NET/Body.cs index 2bcb85e..0355b30 100644 --- a/src/Box3D.NET/Body.cs +++ b/src/Box3D.NET/Body.cs @@ -290,7 +290,10 @@ public unsafe Shape AddCapsule(Capsule capsule, ShapeDefinition? definition = nu /// The new shape. /// /// The box is a convex hull underneath, but it is self-contained, so nothing - /// needs to be released afterwards. + /// needs to be released afterwards. Because it is a hull, the shape reports + /// as its , and behaves as + /// a hull everywhere, including in . There is + /// no need to build a box from eight corners with . /// /// /// diff --git a/src/Box3D.NET/PhysicsWorld.cs b/src/Box3D.NET/PhysicsWorld.cs index 837593a..f8eebd1 100644 --- a/src/Box3D.NET/PhysicsWorld.cs +++ b/src/Box3D.NET/PhysicsWorld.cs @@ -746,7 +746,16 @@ public WorldEvents Events /// The distance beyond the radius over which the impulse fades to nothing. /// Which shape categories are affected. Defaults to all of them. /// The world has been disposed. - /// Only spheres, capsules and hulls respond. + /// + /// + /// Spheres, capsules and hulls respond, and boxes are hulls, so a shape added + /// with responds too. Meshes, height fields and + /// compounds do not: they can only be attached to static bodies, and an + /// explosion only reaches dynamic ones. Kinematic bodies are not pushed + /// either. + /// + /// Sleeping bodies within reach are woken. + /// public void Explode( Vector3 center, float radius, diff --git a/src/Box3D.NET/ShapeDefinition.cs b/src/Box3D.NET/ShapeDefinition.cs index 014ed9d..2d4c6a7 100644 --- a/src/Box3D.NET/ShapeDefinition.cs +++ b/src/Box3D.NET/ShapeDefinition.cs @@ -19,6 +19,10 @@ public enum ShapeType HeightField = 2, /// A convex hull. + /// + /// Boxes are hulls too: a shape added with reports + /// this type, since Box3D has no separate box shape. + /// Hull = 3, /// A triangle mesh. From 2325ce188483c6d0a6581080674bb99b91a0850f Mon Sep 17 00:00:00 2001 From: Miguel Angel Charris Carmona Date: Wed, 23 Sep 2026 15:30:21 -0500 Subject: [PATCH 2/4] Warn about mass ratios across joints A demolition demo hung a ball of about 33 kg from a chain of spherical joints whose links weighed about 0.02 kg, and the links flew off. Nothing in the library is wrong there: the solver resolves joints iteratively, and a light body between an anchor and a heavy one barely moves the heavy one, so the error never converges. But nothing said to avoid it either. Reproduced before writing it down, with ten spherical joints: at about 1450:1 the fastest link reaches 427 m/s and the last joint stretches by 0.66 m; at about 20:1 the fastest link moves at 3.1 m/s and the joint holds to 3 mm. The guide gets a callout recommending 10:1 to 20:1 or less, the measurement behind it, and a note that the ratio that matters is the one across each joint rather than the chain's total mass. The chain sample says the same where its links are created, since that is the code people copy. The measurement is not a test: it pins solver behaviour, which is Box3D's to change, not this binding's. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 7 +++++++ docs/guides/joints.md | 21 +++++++++++++++++++++ src/Box3D.NET.Samples/JointSamples.cs | 3 +++ 3 files changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857b59b..5bceea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `ShapeType.Hull` note that a box reports `ShapeType.Hull`. `ExplosionTests` pins all of it. +- **The joint guide warns about mass ratios.** A chain of light links holding a + heavy body — about 1450:1 across the last joint — lets the links reach + hundreds of metres per second, while 20:1 hangs still. That is the solver, not + the binding, but nothing said so. `docs/guides/joints.md` now recommends + keeping the ratio across each joint to roughly 10:1–20:1 or less, with the + measurement, and the `chain` sample says the same where its links are made. + ## [0.5.0] - 2026-09-23 An engine release, and a minor version for two reasons at once: the idiomatic diff --git a/docs/guides/joints.md b/docs/guides/joints.md index 1bcff95..30943ff 100644 --- a/docs/guides/joints.md +++ b/docs/guides/joints.md @@ -51,6 +51,27 @@ Build the assembly in its rest pose. A chain assembled already displaced has every joint violated on the first step and snaps — give it angular velocity instead. +## Keep mass ratios modest + +> **Keep the mass ratio between jointed bodies modest — roughly 10:1 to 20:1 or +> less.** Light links holding a heavy body jitter, stretch or fly apart. Make +> the links heavier or the load lighter. + +This is how the solver behaves, not a fault in a particular joint. It resolves +each joint iteratively, and a light body between a heavy one and an anchor +barely moves the heavy one, so the error never converges. Measured with a chain +of ten `Spherical` joints, links of radius 0.14 and a ball of radius 1.1: + +| Link density | Ball density | Mass ratio | Fastest link | Worst stretch at the ball | +| ---: | ---: | ---: | ---: | ---: | +| 2 | 6 | about 1450:1 | 427 m/s | 0.66 m | +| 60 | 2.5 | about 20:1 | 3.1 m/s | 0.003 m | + +The masses that matter are the ones either side of each joint, so a chain of +equal links is fine however heavy it is overall; it is the last link against the +load that has to be kept in proportion. Density is the lever: mass follows from +it and the volume. + ## Limits and motors Most joints take limits, a motor, or both. The pattern is the same everywhere: diff --git a/src/Box3D.NET.Samples/JointSamples.cs b/src/Box3D.NET.Samples/JointSamples.cs index 57044d2..7116c6f 100644 --- a/src/Box3D.NET.Samples/JointSamples.cs +++ b/src/Box3D.NET.Samples/JointSamples.cs @@ -107,6 +107,9 @@ public static void Run() { Vector3 position = new(0.0f, 10.0f - ((i + 1) * LinkSpacing), 0.0f); + // Every link weighs the same. Hanging something heavy off the end of + // light links is what makes a chain jitter or fly apart; keep the + // mass ratio across each joint to about 10:1 or 20:1 at most. Body link = world.CreateBody(BodyDefinition.Dynamic(position)); link.AddSphere(new Sphere(0.12f), ShapeDefinition.Default with { Density = 2000.0f }); links[i] = link; From 3ca274f9f119e730a14ad2b1a8a6e471609c0ea4 Mon Sep 17 00:00:00 2001 From: Miguel Angel Charris Carmona Date: Wed, 23 Sep 2026 15:31:45 -0500 Subject: [PATCH 3/4] Add friction and restitution shortcuts to ShapeDefinition Changing one material property on a shape definition took two nested with expressions: ShapeDefinition.Default with { Material = PhysicsMaterial.Default with { Friction = 0.7f } } ShapeDefinition.Friction and Restitution are init properties that read and write Material, so the same thing is ShapeDefinition.Default with { Friction = 0.7f }. Properties rather than WithFriction(float) methods, because nothing in this API builds values through methods: every definition is a record with init properties, adjusted with with, and a shortcut that did not work inside a with expression would sit beside every other property as the one exception. Shape already exposes Friction and Restitution as shortcuts for its material at run time, so the definition now matches the handle. Only these two. Every PhysicsMaterial property could get a shortcut, but friction and restitution are the ones a game tunes per shape; the rest are rarer, and duplicating the whole material would give ShapeDefinition two ways to say everything. The properties have no backing field, so equality and hashing are unchanged: a definition built with a shortcut equals one built the nested way, which ShapeDefinitionTests checks, along with initializer order and that the values reach the native shape. The change is additive, so package validation against the previous release is unaffected. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 9 +++ src/Box3D.NET.Tests/ShapeDefinitionTests.cs | 68 +++++++++++++++++++++ src/Box3D.NET/ShapeDefinition.cs | 43 ++++++++++++- 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/Box3D.NET.Tests/ShapeDefinitionTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bceea9..aef1900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`ShapeDefinition.Friction` and `ShapeDefinition.Restitution`**, shortcuts for + the two material properties changed most often. Changing one used to take a + nested `with`; now `ShapeDefinition.Default with { Friction = 0.7f }` does it. + Both read and write `Material` and store nothing of their own, so a definition + built either way is equal to the other. Initializers run in order, so set + `Material` before them when using both. + ### Changed - **`PhysicsWorld.Explode` says which shapes respond.** It read "only spheres, diff --git a/src/Box3D.NET.Tests/ShapeDefinitionTests.cs b/src/Box3D.NET.Tests/ShapeDefinitionTests.cs new file mode 100644 index 0000000..c9d1ffc --- /dev/null +++ b/src/Box3D.NET.Tests/ShapeDefinitionTests.cs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +using Xunit; + +namespace Box3D.Tests; + +/// +/// Covers the friction and restitution shortcuts on . +/// +[Collection(NativeCollection.Name)] +public class ShapeDefinitionTests +{ + [NativeFact] + public void The_friction_shortcut_changes_only_the_friction() + { + ShapeDefinition def = ShapeDefinition.Default with { Friction = 0.7f }; + + Assert.Equal(0.7f, def.Friction); + Assert.Equal(PhysicsMaterial.Default with { Friction = 0.7f }, def.Material); + } + + [NativeFact] + public void The_restitution_shortcut_changes_only_the_restitution() + { + ShapeDefinition def = ShapeDefinition.Default with { Restitution = 0.8f }; + + Assert.Equal(0.8f, def.Restitution); + Assert.Equal(PhysicsMaterial.Default with { Restitution = 0.8f }, def.Material); + } + + [NativeFact] + public void A_shortcut_is_the_same_definition_as_the_nested_form() + { + ShapeDefinition shortcut = ShapeDefinition.Default with { Friction = 0.7f, Restitution = 0.2f }; + ShapeDefinition nested = ShapeDefinition.Default with + { + Material = PhysicsMaterial.Default with { Friction = 0.7f, Restitution = 0.2f }, + }; + + // Nothing is stored outside Material, so equality and hashing agree. + Assert.Equal(nested, shortcut); + Assert.Equal(nested.GetHashCode(), shortcut.GetHashCode()); + } + + [NativeFact] + public void A_shortcut_after_a_material_refines_it() + { + var ice = PhysicsMaterial.Default with { Friction = 0.02f, UserMaterialId = 7 }; + + ShapeDefinition def = ShapeDefinition.Default with { Material = ice, Restitution = 0.5f }; + + Assert.Equal(0.02f, def.Friction); + Assert.Equal(0.5f, def.Restitution); + Assert.Equal(7UL, def.Material.UserMaterialId); + } + + [NativeFact] + public void The_shortcuts_reach_the_shape() + { + using var world = new PhysicsWorld(); + Body body = world.CreateDynamicBody(); + + Shape shape = body.AddBox(Box.Cube(0.5f), ShapeDefinition.Default with { Friction = 0.7f, Restitution = 0.3f }); + + Assert.Equal(0.7f, shape.Friction); + Assert.Equal(0.3f, shape.Restitution); + } +} diff --git a/src/Box3D.NET/ShapeDefinition.cs b/src/Box3D.NET/ShapeDefinition.cs index 2d4c6a7..f5c59bc 100644 --- a/src/Box3D.NET/ShapeDefinition.cs +++ b/src/Box3D.NET/ShapeDefinition.cs @@ -52,7 +52,7 @@ public enum ShapeType /// var def = ShapeDefinition.Default with /// { /// Density = 2000.0f, -/// Material = PhysicsMaterial.Default with { Friction = 0.9f }, +/// Friction = 0.9f, /// EnableContactEvents = true, /// }; /// @@ -80,6 +80,47 @@ public readonly record struct ShapeDefinition /// Gets the surface properties. public PhysicsMaterial Material { get; init; } + /// Gets the friction of . + /// + /// + /// A shortcut for the common case of changing only the friction, which + /// otherwise takes a nested with. Setting it replaces the material + /// with a copy that differs only in friction; nothing is stored apart from + /// . + /// + /// + /// Initializers run in order, so when setting both, set + /// first: a after this one + /// replaces the friction it set. + /// + /// + /// + /// + /// var grippy = ShapeDefinition.Default with { Friction = 0.9f }; + /// // Same as: + /// var nested = ShapeDefinition.Default with + /// { + /// Material = PhysicsMaterial.Default with { Friction = 0.9f }, + /// }; + /// + /// + public float Friction + { + get => Material.Friction; + init => Material = Material with { Friction = value }; + } + + /// Gets the restitution, or bounciness, of . + /// + /// A shortcut in the same way as , and with the same + /// caveat: set first when setting both. + /// + public float Restitution + { + get => Material.Restitution; + init => Material = Material with { Restitution = value }; + } + /// Gets the collision filtering rules. public CollisionFilter Filter { get; init; } From b695aa8bf5104da95ba6df604f5f25b9d096f910 Mon Sep 17 00:00:00 2001 From: Miguel Angel Charris Carmona Date: Wed, 23 Sep 2026 15:32:49 -0500 Subject: [PATCH 4/4] Document Explode's filter as the category mask it is Explode takes ulong filter = ulong.MaxValue and assigns it to b3ExplosionDef.maskBits, while every query takes a QueryFilter?. The name suggests the latter, and the documentation, "which shape categories are affected", did not say how. b3World_Explode passes the mask to the dynamic tree, which compares it with each proxy's category bits. That is the job QueryFilter.CollidesWith does in a query, but a query also requires the shape's own mask to accept the query's categories (b3ShouldQueryCollide checks both directions), and an explosion does not. The parameter now says all of this, and a test shows a shape whose CollidesWith is zero is still pushed. The parameter keeps its name: renaming it would break anyone passing it as filter: in source. There is no QueryFilter overload either. It would have to drop QueryFilter.Categories on the floor, and an overload that silently ignores half its argument is worse than a mask parameter whose documentation is clear. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 8 ++++++++ src/Box3D.NET.Tests/ExplosionTests.cs | 16 ++++++++++++++++ src/Box3D.NET/PhysicsWorld.cs | 9 ++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aef1900..8d5bccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 keeping the ratio across each joint to roughly 10:1–20:1 or less, with the measurement, and the `chain` sample says the same where its links are made. +- **`Explode`'s `filter` is documented as the mask it is.** The name suggests a + filter type, and the queries take a `QueryFilter`, but this is a `ulong` + compared with each shape's `CollisionFilter.Categories`, in that direction + only: the shape's own `CollidesWith` is not consulted. The parameter keeps its + name, since renaming it would break callers passing it by name. No + `QueryFilter` overload was added, because it would have to ignore + `QueryFilter.Categories` without saying so. + ## [0.5.0] - 2026-09-23 An engine release, and a minor version for two reasons at once: the idiomatic diff --git a/src/Box3D.NET.Tests/ExplosionTests.cs b/src/Box3D.NET.Tests/ExplosionTests.cs index bd87927..115cd6a 100644 --- a/src/Box3D.NET.Tests/ExplosionTests.cs +++ b/src/Box3D.NET.Tests/ExplosionTests.cs @@ -121,4 +121,20 @@ public void The_filter_is_a_category_mask() Assert.True(affected.LinearVelocity.X > 0.1f, $"the debris should be pushed, velocity {affected.LinearVelocity}"); Assert.Equal(Vector3.Zero, spared.LinearVelocity); } + + [NativeFact] + public void The_filter_ignores_what_the_shape_collides_with() + { + // Unlike a query, which also needs the shape to accept the query's + // categories, an explosion checks one direction only. A shape that + // collides with nothing is still pushed. + var aloof = ShapeDefinition.Default with { Filter = CollisionFilter.Default with { CollidesWith = 0 } }; + + Body body = _world.CreateDynamicBody(new Vector3(2.0f, 0.0f, 0.0f)); + body.AddBox(Box.Cube(0.5f), aloof); + + Blast(); + + Assert.True(body.LinearVelocity.X > 0.1f, $"velocity {body.LinearVelocity}"); + } } diff --git a/src/Box3D.NET/PhysicsWorld.cs b/src/Box3D.NET/PhysicsWorld.cs index f8eebd1..3035204 100644 --- a/src/Box3D.NET/PhysicsWorld.cs +++ b/src/Box3D.NET/PhysicsWorld.cs @@ -744,7 +744,14 @@ public WorldEvents Events /// The impulse per unit of facing area. Negative values implode. /// /// The distance beyond the radius over which the impulse fades to nothing. - /// Which shape categories are affected. Defaults to all of them. + /// + /// A category mask, not a : a shape is affected when + /// this has a bit in common with its . + /// It plays the part of in a query, but + /// only in that direction; the shape's own + /// is not consulted. Defaults to + /// every category. + /// /// The world has been disposed. /// ///