Cube/Plane not Simulating in ECS-based Physics Setup
ScottKane opened this issue · 7 comments
I'm trying to get started with a simple scenario, a cube to drop onto a plane. I'm using an ECS so I have split everything out to work with that. From what was shown in the self contained demo I'm assuming this should be all I need to get things running simply, however nothing happens when I run the scene. Note the cube.obj is 2x2x2 which is why I have the weird scale/box sizings. Any advice would be greatly appreciated. Thanks
Application
.Default()
.WithEntity(entity =>
{
entity.Set(new Transform
{
Position = new Vector3(0.0f, 0.0f, -5.0f),
Scale = new Vector3(0.5f)
});
entity.Set(Model.Load("Models/cube.obj", new Vector3(1, 0, 0)));
entity.Set(new Box(1, 1, 1));
})
.WithEntity(entity =>
{
entity.Set(new Transform
{
Position = new Vector3(0.0f, -1.5f, -5.0f),
Scale = new Vector3(10.0f, 0.01f, 10f)
});
entity.Set(Model.Load("Models/cube.obj"));
entity.Set(new Box(20.0f, 0.02f, 10f));
entity.Set(new Static());
})
.WithSystem<MoveCamera>()
.Run();
public sealed class PhysicsPlugin : IPlugin
{
public IApplication Configure(IApplication application) =>
application
.WithSystem<CreatePhysics>(Stage.Startup)
.WithSystem<AddPhysicsBodies>(Stage.Startup)
.WithSystem<AddPhysicsStatics>(Stage.Startup)
.WithSystem<DeletePhysics>(Stage.Shutdown)
.WithSystem<UpdatePhysics>(Stage.FixedUpdate);
}
public struct Physics
{
public BufferPool BufferPool;
public Simulation Simulation;
}
public class CreatePhysics : WorldSystem
{
protected override void Run(double delta)
{
var bufferPool = new BufferPool();
var physics = new Components.Physics
{
BufferPool = bufferPool,
Simulation = Simulation.Create(bufferPool, new NarrowPhaseCallbacks(), new PoseIntegratorCallbacks(new Vector3(0, -10, 0)), new SolveDescription(8, 1))
};
World.Set(physics);
}
private struct NarrowPhaseCallbacks : INarrowPhaseCallbacks
{
public void Initialize(Simulation simulation) { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AllowContactGeneration(int workerIndex, CollidableReference a, CollidableReference b, ref float speculativeMargin) => a.Mobility == CollidableMobility.Dynamic || b.Mobility == CollidableMobility.Dynamic;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AllowContactGeneration(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB) => true;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ConfigureContactManifold<TManifold>(int workerIndex, CollidablePair pair, ref TManifold manifold, out PairMaterialProperties pairMaterial) where TManifold : unmanaged, IContactManifold<TManifold>
{
pairMaterial.FrictionCoefficient = 1f;
pairMaterial.MaximumRecoveryVelocity = 2f;
pairMaterial.SpringSettings = new SpringSettings(30, 1);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB, ref ConvexContactManifold manifold) => true;
public void Dispose() { }
}
private struct PoseIntegratorCallbacks : IPoseIntegratorCallbacks
{
public void Initialize(Simulation simulation) { }
public readonly AngularIntegrationMode AngularIntegrationMode => AngularIntegrationMode.Nonconserving;
public readonly bool AllowSubstepsForUnconstrainedBodies => false;
public readonly bool IntegrateVelocityForKinematics => false;
private readonly Vector3 _gravity;
public PoseIntegratorCallbacks(Vector3 gravity) : this() => _gravity = gravity;
private Vector3Wide _gravityWideDt;
public void PrepareForIntegration(float dt) => _gravityWideDt = Vector3Wide.Broadcast(_gravity * dt);
public void IntegrateVelocity(Vector<int> bodyIndices, Vector3Wide position, QuaternionWide orientation, BodyInertiaWide localInertia, Vector<int> integrationMask, int workerIndex, Vector<float> dt, ref BodyVelocityWide velocity) => velocity.Linear += _gravityWideDt;
}
}
[Without<Static>]
public class AddPhysicsBodies : EntitySystem<Box, Transform>
{
protected override void Run(double delta, in Entity entity, ref Box box, ref Transform transform)
{
var simulation = World.Get<Components.Physics>().Simulation;
simulation.Bodies.Add(BodyDescription.CreateDynamic(transform.Position, box.ComputeInertia(1), simulation.Shapes.Add(box), 0.01f));
}
}
public class AddPhysicsStatics : EntitySystem<Box, Transform, Static>
{
protected override void Run(double delta, in Entity entity, ref Box box, ref Transform transform, ref Static _)
{
var simulation = World.Get<Components.Physics>().Simulation;
simulation.Statics.Add(new StaticDescription(transform.Position, simulation.Shapes.Add(box)));
}
}
public class UpdatePhysics : ComponentSystem<Components.Physics>
{
protected override void Run(double delta, ref Components.Physics physics) => physics.Simulation.Timestep((float)delta);
}
public class DeletePhysics : ComponentSystem<Components.Physics>
{
protected override void Run(double delta, ref Components.Physics physics)
{
physics.Simulation.Dispose();
physics.BufferPool.Clear();
}
}
Nothing jumps out at a glance; I'd verify that:
- The timestep is actually being called,
- The IntegrateVelocity callback is invoked,
- Double check that the graphical representation is actually getting the updated results from physics (e.g. check if the physics is changing while the graphics are not).
The timestep is definitely being called and the IntegrateVelocity callback is being called. The Transform.Position isn't being updated though. That's what I'm passing in as the RigidPose. Having a system manually moving the entity updated the graphics as expected.
Do I need to manually set my transforms position to the pose position from the body after each physics update? I think RigidPose would have to be a ref struct with Position as a ref field for it to update the position being passed in. How is this normally meant to work? You take a handle to the body and then use the pose position during rendering?
Yup, RigidPose
and Vector3
and such are regular ol' value types and pass by copy, so no updates will feed back through it. Simulation.Bodies.Add
returns a BodyHandle
, and you can get a BodyReference
to the body with Simulation.Bodies[bodyHandle]
. It's a convenience type that provides access to all the body properties, including pose. (You can also do the indirections and go look up data directly, or temporarily cache pointers for accesses, but that's a bit more annoying and usually unnecessary.)
Ok so now I have added a system to copy over the Position
Application
.Default()
.WithEntity(entity =>
{
entity.Set(new Transform
{
Position = new Vector3(0.0f, 0.0f, -5.0f)
});
entity.Set(Model.Load("Models/crate.glb"));
entity.Set(new Box(1, 1, 1));
})
.WithEntity(entity =>
{
entity.Set(new Transform
{
Position = new Vector3(0.0f, -1.5f, -5.0f),
Scale = new Vector3(10.0f, 0.01f, 10f)
});
entity.Set(Model.Load("Models/cube.obj"));
entity.Set(new Box(500, 1, 500));
entity.Set(new Static());
})
.WithSystem<MoveCamera>()
.WithSystem<LogTransform>(Stage.FixedUpdate)
.Run();
public class UpdateTransform : EntitySystem<BodyHandle, Transform>
{
protected override void Run(double delta, in Entity entity, ref BodyHandle handle, ref Transform transform)
{
var simulation = World.Get<Components.Physics>().Simulation;
transform.Position = simulation.Bodies[handle].Pose.Position;
}
}
This produces:
Transform { Position = <-65.96129, 5.8624425, -39.0519>, Rotation = <0, 0, 0>, Scale = <1, 1, 1> }
Transform { Position = <-187.52643, -0.71619415, -111.09422>, Rotation = <0, 0, 0>, Scale = <1, 1, 1> }
Transform { Position = <-187.3982, -0.4079471, -110.98699>, Rotation = <0, 0, 0>, Scale = <1, 1, 1> }
Transform { Position = <-190.50215, -1.4001498, -112.26572>, Rotation = <0, 0, 0>, Scale = <1, 1, 1> }
Considering the cube starts at new Vector3(0.0f, 0.0f, -5.0f)
according to the transfrom, I'm not sure how the first fixed update tick is changing the position so drastically. These positions don't indicate that the cube is falling straight down like it should be, do you have any idea what could be happening?
Nope! It's hard to tell given the layers of indirection involved, but clearly something is wonky.
Double check the time passed to Timestep
; it should be the same every time (in this use case). Peek at the body states after each timestep to see when things go off the rails.
If all else fails, try to reproduce it in the demos. It'll tend to make the problem obvious, or the failure to reproduce the problem will be informative.
omfg... the timestep was 16.666
instead of 0.01666
which just broke everything. Doing 1000 / 60
instead of 1 / 60
...