Sunday, 30 April 2017

Adding Simple Physics

Next up we can add some simple physics handling to the basic map and model loads under Urho3D, and this lets us handle simple objects and push cars round the map.

Introducing Physics

Urho uses the Bullet Physics library to provide a physics engine that supports collision detection and rigid body simulation.

Urho embeds a complete copy of Bullet under the Source/ThirdParty/Bullet/ directory, and there is a subset of the functionality exposed through AngelScript. However the AngelScript API doesn't support the complete feature set of Bullet that the C++ code can access, which will get annoying (as we see later).

Adding A Simple Colliding Entity

So initially we have to:

Enable Physics

This is a single line in the AngelScript

 thescene.CreateComponent("PhysicsWorld");

Turn on Collisions With the Scene

This is fairly simple we have the Node that the terrain Component was created in, and we want to tell the Physics engine that objects should collide with this terrain. To create this behaviour the engine wants us to do the following under the Terrain node:

  • Create a Rigid Body: A solid body, which doesn't deform, and has basic physics properties (such as mass and friction).
  • Add a Collision Shape: To support collisions, marked as Terrain in this case.

So, first we make a default RigidBody associated with the Terrain Node as a fixed object (the default, for a zero mass item).

RigidBody@ body = terrainNode.CreateComponent("RigidBody");
  body.collisionLayer = 2;
  

The collisionLayer parameter provides a bitmask that the Bullet engine uses to determine if objects should collide. The tutorials use the mask value of 2 throughout for terrain, and we'll keep that convention here.

There are actually two parameters here: collisionLayer and collisionMask. This is a detail we won't get into since it's not something that affects this demo, but when Objects 1 & 2 intersect then we evaluate:

  • the object 1 mask and object 2 collision layer
  • the object 2 mask and object 1 collision layer
If either of these operations are non-zero then we have a collision, otherwise they pass through each other.

Next we associate a collision shape - this is created alongside the static Rigid Body under the terrain node. The shape of this collider is marked as Terrain.

CollisionShape@ shape = terrainNode.CreateComponent("CollisionShape");
  shape.SetTerrain();
And that's it.

Add Shadows

Not necessary, but the relation of objects in the world is much clearer with shadows - otherwise entities have a tendency to look painted over, rather than on, the terrain. So we have to enable light shadows when we declare our light:

light.castShadows = true;
And when we create an object we'll also set the model castShadows property to true.

And The Object

We can add a simple colliding sphere (we'll call it a Marble), by creating a node with a Static Model based on "Sphere.mdl", and then adding the RigidBody and CollisionShape declaration.

For this example we use SetSphere() to set the collision shape to match the (spherical) model, and we give the RigidBody a mass and rolling friction and watch it go. Here's a complete working class, based on the code in 11_Physics.as:

class Marble {
Node@ marbleNode;
StaticModel@ mdlObj;
RigidBody@ body;
CollisionShape@ shape;

bool doPrint = false;

  Marble(Scene@ scene, float sz, float speed, float mass) {
    Create(scene, sz, speed, mass);
  }

  void Create(Scene@ scene, float sz, float speed, float mass) {
    marbleNode = scene.CreateChild();
    marbleNode.position = camera.cameraNode.position;
    marbleNode.rotation = camera.cameraNode.rotation;
    marbleNode.SetScale(sz);

    mdlObj = marbleNode.CreateComponent("StaticModel");
    mdlObj.model = cache.GetResource("Model", "Models/Sphere.mdl");
    mdlObj.material = cache.GetResource("Material", "Materials/Terrain.xml");
    mdlObj.castShadows = true;

    body = marbleNode.CreateComponent("RigidBody");
    body.mass = mass;
    body.rollingFriction = 0.9f;

    shape = marbleNode.CreateComponent("CollisionShape");
    shape.SetSphere(1.0f);

    body.linearVelocity = camera.cameraNode.rotation * Vector3(0.0f, 0.0f, 1.0f) * speed;

    SubscribeToEvent(marbleNode, "NodeCollision", "HandleNodeCollision");
  }

  void HandleNodeCollision(StringHash eventType, VariantMap& eventData)  {
    if (doPrint) {
    RigidBody@ otherBody = eventData["OtherBody"].GetPtr();
    RigidBody@ hitBody = eventData["Body"].GetPtr();
      Print("Ding "+ hitBody.id +" against "+ otherBody.id + " !");
    }
  }
}
This provides the basic object and also an optional debugging report on collisions.

We can play around with this code and check the behaviour of the physics simulation. There are some problems when we fire very small and very fast marbles which have a tendency to fall through the terrain or when we combine very heavy and very light objects - this is expected and the Bullet and Urho docs mention the need to handle these cases carefully.

Adding A Car

There's an example of a simple vehicle provided by the Urho file ./bin/Data/Scripts/19_VehicleDemo.as. It's worth taking a moment to look at how this works.

The car consists of a main hull body and four wheels. The main body is a RigidBody/CollisionShape and only has mass and air resistance, and all the power and steering is done by manipulating the wheels directly.

Each wheel is a cylinder model with an associated RigidBody and a spherical CollisionShape connected to the main body via a Hinge Constraint.

The hinge constraint is used to link the body to the wheel, and the wheel spins around this constraint. The hinge constraint limits are set from -180 to +180 degrees which allow the wheel to spin completely.

The constraint connecting the wheel to the body is also used for steering: by rotating the angle of the connection between the body and the wheel, the code deflects the front two wheels in response to steering commands.

Although the hinge is restricting motion around other axis there are some damped-spring style reactions to forces which can be seen at higher mass main body values, although I suspect this may be a bug involving the mass ratio of a light wheel to a heavy body. It's actually almost a usable hack for suspension style behaviour.

By default the reference code subclasses ScriptObject, which allows it to serialise the object to a file, and to handle control events directly. We can actually fire the event handler explicitly and break this connection if we want to simplify the implementation and don't care about serialising.

When accelerating the ApplyTorque() function is used to change the angular velocity of the wheel. In addition there's a directional component introduced to the front wheel (when steering is turned). Actual acceleration results from the interaction between the rotating wheels and terrain.

Lastly there's a downforce component which keeps the car connected to the terrain, by applying a downward force based on the car velocity.

Updating The Car

Although the code works with a basic setup, it's difficult to control and tune, and doesn't work when exposed to the kind of terrains we see in I76.

Firstly we need lower the downforce component. This works well to glue the car to the rolling terrain, but it tends to let the car drive down cliff faces on the I76 maps.

Next we modify the loaded model to be one of the one piece I76 car geometries, and tweak the wheel positions and collision bounds accordingly. We also move the body mass up to a more realistic (real world) value of around 1500 (assuming 1 unit = 1kg).

To simplify the control of acceleration we replace the "Torque" model of the demo with a simple ApplyImpulse() version. This doesn't rely on the torque for acceleration, but pushing the wheels directly. We put a simple check in to ensure that the wheel is in collision with the terrain before applying force through it, but that's all. This gives a simpler more controllable vehicle, although it's a bit like driving on glass.

How This Needs To Improve

Right now there's no suspension, or notion of vehicle simulation such as engine or tyre behaviour, weight transfer, etc. which would make the driving behaviour more like the original game.
We could add some of this in AngelScript, but that has an obvious overhead in terms of processing load and even a simplified model is a very complex problem. So are there any other options?

The good news is that the bullet physics library provides a complete abstraction - the btRayCastVehicle model. This supports a single solid body type which behaves like a vehicle, has a tunable suspension and wheel model, and takes care of all the details for us. There's a great example of it in action in this GitHub repository by the Urho user Lumak.

However the downside of the Bullet vehicle is that this is currently a C++ only thing - it's not exposed to the AngelScript interfaces. This is an annoyance, since using AngelScript to keep things cross platform is a definite advantage.

So we've hit a break where I need to figure out where to go next - Is there a plausible I76-like car behaviour without too much scripting code using a few approximations, or is native code for the car the way to go, or some mix and custom scripting interfaces down to the library? At this point experimentation will be required though, so that'll do for now...

Update - having just pulled the latest from the Urho3D Repo it turns out that RayCast vehicle integration & scripting hooks have been merged into the main release as of 20-something hours ago. So this decision is probably simpler than I thought.

No comments: