Novolis Docs
novolis-physics / INTEGRATION.md

Integration guide

dotnetphysicssimulationnovolis

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

GoalAPI
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).

PieceAPI
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).

BehaviorWhen
Reliable hitDisplacement 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):

PieceRole
`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.

PieceRole
`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

GoalUse
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`