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 |