Integration guide
Force-first textbook physics
Novolis.Physics is force-first: IForceModel computes forces, SimulationPipeline sums them, IIntegrator advances state.
Related: ARCHITECTURE.md · examples/ · README.md
Pass simulation time explicitly on each Step: the pipeline does not advance timeSeconds for you — use timeSeconds + dtSeconds after each fixed step (see README quick start).
1. Rigid body (recommended default)
var pipeline = new SimulationPipeline<RigidBodyState, PointMassField>(
new SemiImplicitEulerRigidBodyIntegrator(),
new PointMassGravityModel());
var field = new PointMassField([(position, gm), ...]);
double time = 0;
body = pipeline.Step(body, field, dtSeconds, time);
time += dtSeconds;Add more forces to the pipeline constructor: SimpleLiftDragModel, custom IForceModel implementations, etc.
Use FixedStepAccumulator to drain variable frame time into fixed physics steps.
2. Ballistics
| Goal | API |
|---|---|
| Cannon / quick prototype | `ProjectileBallisticSimulation` |
| Custom forces / composition | `SimulationPipeline<ProjectileState, TEnv>` + `ProjectileSemiImplicitIntegrator` + `ProjectileQuadraticDragModel` (+ gravity `IForceModel`) |
Facade (simplest): ProjectileBallisticSimulation — uniform −Y gravity and optional quadratic drag.
Pipeline (extensible): compose IForceModel instances in SimulationPipeline<ProjectileState, TEnv>.
For default uniform gravity and quadratic drag, the facade and pipeline are equivalent (see ProjectileDragPipelineParityTests in the unit project).
BallisticsQueries.SweepProjectileSphere is a discoverability wrapper over IStaticWorld.SweepSphere for projectile-sized spheres.
Convention: +Y up, range often along +X, set Z = 0 for planar cannon problems.
Terrain flight loop
For projectile vs heightfield + triangle mesh, use `ProjectileTerrainStepper` or stateful `BallisticTrajectoryRunner` — do not hand-roll integrate-then-sweep unless you match ProjectileSemiImplicitIntegrator displacement (candidate.Position - startPos).
| Piece | API |
|---|---|
| Height sampling | `IHeightSampler` |
| Range box | `AxisAlignedRangeBox` + `IProjectileTerrainContact` (e.g. `BoundedHeightfield` in Simulation.World) |
| One physics step | `ProjectileTerrainStepper.AdvanceOne` |
| Full shot + trail | `BallisticTrajectoryRunner` |
| Aim preview (no mesh) | `BallisticTrajectoryRunner.BuildPreview` |
Do not use GroundImpact / ProjectileMath.InterpolateGroundImpact for arbitrary terrain — those assume Y = 0 plane only.
Full examples: examples/ballistics.md.
3. Aerodynamics (pipeline)
Add lift/drag on rigid bodies via SimpleLiftDragModel and an atmosphere hook:
using Novolis.Physics.Abstractions;
using Novolis.Physics.Aerodynamics;
using Novolis.Physics.Gravity;
using Novolis.Physics.Motion;
using System.Numerics;
var atmosphere = new ExponentialAtmosphereModel(seaLevelDensityKgPerM3: 1.225, scaleHeightMeters: 8500);
var aeroEnv = new SimpleAeroEnvironment(
atmosphere,
altitudeMeters: body.Position.Y,
windWorld: Vector3.Zero,
referenceAreaM2: 2.0,
dragCoefficient: 0.35,
liftCoefficient: 0.8,
liftReferenceForwardWorld: Vector3.Transform(Vector3.UnitZ, body.Orientation));
var pipeline = new SimulationPipeline<RigidBodyState, SimpleAeroEnvironment>(
integrator,
gravity,
new SimpleLiftDragModel());IAtmosphereModel.DensityAtAltitude supplies ρ(h); wind and coefficients live in SimpleAeroEnvironment. The model is time-invariant (ignores timeSeconds).
4. Collision (query + sphere integrator)
IStaticWorld (BvhStaticWorld, EmptyStaticWorld) provides raycast and approximate sphere/capsule sweeps. Not a full rigid-body engine.
For a bouncing sphere in a static mesh, use BvhStaticSphereIntegrator.AdvanceOneStep (or AdvanceWithUniformAccelerationAndLinearDrag) alongside your gravity model. Contact resolution is handled inside the integrator via SphereContactKinematics.ReflectWithRestitution (see Novolis.Physics.Collision.Simple).
Sweep limitations
BvhStaticWorld.SweepSphere performs a radius-inflated raycast along the displacement direction (not continuous CCD).
| Behavior | When |
|---|---|
| Reliable hit | Displacement per step is small vs mesh features; shallow penetration near a surface |
| May return **no hit** | Displacement overshoots the first contact (`adjusted > displacement length`); fast motion tunneling past thin geometry; `SweepCapsule` only samples **endpoint spheres** |
Mitigation: smaller physics steps, larger sphere radius margin, or custom CCD for critical paths.
Examples in the unit project:
- Partial-travel hit:
CollisionSweepScenarioTests.SweepProjectileSphere_HitsGroundTriangle - Large-step miss with sub-step hit:
SweepLimitationScenarioTests.SweepSphere_LargeStepOvershoot_MissesWhileSubStepsHit
Example walkthrough: examples/collision-room.md.
5. Sphere ragdoll / joints
For chained equal-radius spheres (ragdolls, rope-like piles):
| Piece | Role |
|---|---|
| `DistanceJoint` + `DistanceJointSolver` | Maintain rest length between sphere indices |
| `SwingLimit` / `HingeLimit` | Angular cones and hinge arcs; use `CreateLocal` + `FrameReferenceSphere` so limits follow the torso |
| `BoneFrame` | Parent-local rest directions from parent + reference sphere positions |
| `ConstrainedSphereSimulator` | Integrates spheres against a static BVH, solves joints + optional angular limits + filtered self-collision |
| `RagdollHumanoidPreset` | 11-sphere humanoid topology, standing spawn, and limit set |
var sim = new ConstrainedSphereSimulator { Options = { Radius = 0.2f, ... } };
RagdollHumanoidPreset.BuildStanding(groundPoint, spheres, joints, swings, hinges);
sim.SetJoints(joints);
RagdollHumanoidPreset.StabilizeSpawn(spheres, joints, clamp, sim);
// each frame:
sim.Step(world, spheres, clamp, dt, swings, hinges);Self-collision skips joint-adjacent pairs automatically when joints are set via SetJoints.
5b. Cloth (`Novolis.Physics.Cloth`)
Fabric simulation lives in `Novolis.Physics.Cloth`, not Joints. Ragdolls stay in Joints; cloth reuses DistanceJoint as a shared length primitive but steps with fabric strain limits, wind, and cutting.
| Piece | Role |
|---|---|
| `ClothSheetOptions` / `ClothPinMode` | Columns, rows, spacing, stiffness, pins |
| `ClothSheetPreset` | Spawn particles + joints + anchors |
| `ClothSheetSimulator` | Integrate → project → `MaxStretchRatio` clamp |
| `ClothCutOps` + `ClothBlade` / `ClothBlast` | Topology sever (sword / explosion path) |
using Novolis.Physics.Cloth;
using Novolis.Physics.Joints;
var cloth = new ClothSheetSimulator { MaxStretchRatio = 1.06f };
ClothSheetPreset.BuildHanging(...);
cloth.SetJoints(joints);
cloth.Step(world, spheres, clamp, dt);
ClothCutOps.CutWithBlade(joints, spheres, new ClothBlade(heel, tip));Dogfood: d:\novolis\novolis-dogfooding\apps\ClothPlay.
6. Orbits (separate stack)
CentralOrbitSimulator / LeapfrogCentralBodySoA use symplectic leapfrog for central-body problems. Does not plug into SimulationPipeline. Use Novolis.Physics.Gravity point-mass models for game-style gravity instead.
For repeated propagation, reuse one LeapfrogCentralBodySoA via CentralOrbitSimulator.SimulateFor(initial, integrator, bodyIndex, ...) instead of the convenience overload that allocates each call.
Decision tree
| Goal | Use |
|---|---|
| Rigid body + arbitrary forces | `SimulationPipeline` + `SemiImplicitEulerRigidBodyIntegrator` |
| Cannon / projectile with drag | `ProjectileBallisticSimulation` or ballistics pipeline |
| Sphere in a static room | `BvhStaticSphereIntegrator` + mesh world |
| Ragdoll / jointed sphere chain | `ConstrainedSphereSimulator` + `RagdollHumanoidPreset` (`Novolis.Physics.Joints`) |
| Cloth sheet / flag / drape | `ClothSheetSimulator` + `ClothSheetPreset` (`Novolis.Physics.Cloth`) |
| Cloth cut / sword / blast | `ClothCutOps` (`Novolis.Physics.Cloth`) |
| Long-term two-body orbit test | `CentralOrbitSimulator` |