Physical surface properties in ODE

ODE, the physics library that is used in Luxinia can simulate the physical behavior of objects, which means that it takes care of the movement, collision detection and the collision reactions.

Just as Luxinia distinguishes between the object's position in space and it's way how it is drawn (actors ↔ l3dnodes), ODE distinguishes between the simulated object and it's geometry.

The simulated objects, which take care about the movement, the mass, the rotational moment and so on are called bodies, while the geometry which defines the physical size and form of the object are called geoms.

L3dnodes are linked with actors, actors with bodies and geoms with bodies. Once we linked these four parts, we can see the object and can simulate in a realistic manner.

Just as the l3dnodes define the visible appearance, the geoms define it's physical appearance, which includes it's volume it is using in space as same as how to react in case of a collision.

Once a collision was detected, a so called contact joint is created, which simulated the actual collision and is pushing the objects in a way that the collision is solved - if the geoms are penetrating each other, the collision joints try to push the bodies that are connected with the geoms in a way that the penetration is solved, shortly, the objects will bounce off (this leads to the idea that it does not make any sense at all to test geometry with each other that is not connected with bodies, since the geometry is static then - this is why we put such static geometry into a space and switch off the collision detection).

The contact joint requires quite a lot of information about how the surface looks like - should it be bouncy, soft, and so on?

These parameters are defined when the contact joints are created, and Luxinia offers a way how to define them.

Collision surface properties

When two objects interact in a collision, the surface can behave in very different ways. For instance, the collision between rubber and ice differs from the collision of rubber and stone. Stone and ice will also require their own collision parameters.

Since there is no effective way how to break down such a system in simple variables for all cases, Luxinia offers to create up to 256 different parameter sets for setting the contact joint parameters. Each geom has its own surface id which also ranges between 0 and 256. We can then map the collision between two geoms to a certain parameter set using a table. The table defines which set is to be used when a surface material A is colliding with a surface material B:

a = 1
b = 2
parameterset = 3
dworld.surfacecombination(a,b, parameterset)

The surface ids 1 and 2 are mapped to the parameter set 3 in this case (per default, all collisions map to parameter set 0). It is not distinguished between a collision of 2 with 1 or 1 with 2 (this wouldn't make much sense...).

The parameters of the 256 different sets are then set in the dworld class, for example setting the friction value to a certain value is done this way:

parameterset = 3
dworld.surfacemu(parameterset, .01) -- low friction

ERP, CFM, FDIR

While the friction variable is a quite simple parameter, ODE also offers a number of other parameters. These parameters can be switched on or off, using the bit modifiers which can be set in the parameter set (i.e. dworld.surfacebitmu2 enables the mu2 value). Switching off certain parameters have a positive influence on the simulation speed when many contact joints are created. Per default, the ERP and CFM parameters are activated but may be switched off to improve the simulation speed.

The most important parameters are:

  • softERP: The error reduction parameter. It defines how strong an object is pushed back if its geoms are intersecting with other geoms. The value should be ranged between 0 and 1. If set at 1, the object is strongly pushed back if it penetrates another geom, if set to 0, it will slowly sink into the other object if a constant force is applied (like gravity).
  • softCFM: The constraint force mixing value defines how strong the forces are that are applied to object when they penetrate, however it is damping the influence. Setting it to 0 will make the constraint strong, which means that the full force is applied. Setting it to a larger value (i.e. 0.1) will damp the influence of forces at each other.
  • FDIR the first friction direction vector, which points into the direction of the surface. For example, a skid had a direction vector in which it either breaks or slids without too much force

The softERP and softCFM values of the contacts can be used to simulate soft bodies, either bouncy ones or sponge like ones.

Fdir

The first friction direction value is rather interesting since it allows us to set different friction values for two directions. It also seems to stabilize objects that are sliding on the ground, which is useful when a car brake is blocking the wheels. Setting the fdir vector helps to stabilize the car then.

However, the fdir vector is set when the contact is created and thus we cannot define it in the material since it depends on the object properties. For this and some other reasons, we can define a normal vector to the fdir vector, which is calculated then. For example if we have a sphere that should interact as a wheel, we can set the fdir normal vector to point in direction of the wheel's axis.

Once the material has set the fdir bit to true which enables this calculation (which is rather expensive) we can also set the second friction value mu2, which must also be enabled then. Note: setting any of the friction values to 0 will make the other friction value obsolete since 0*anything = 0.

Sample source code

The scene which can be seen in the youtube video is created by a fairly simple script that can be downloaded here.

I will not cover the default scene setup again, instead I will start how the physic scene is created:

globalspace = dspacehash.new() -- our space for everything

objects = {} -- list of objects


function makebody (p)
    -- our helper function takes a table as argument with
    -- all the information we need. Using a table makes it
    -- simpler to extend the arguments to pass and makes
    -- the calling code more readable.

    local actor = actornode.new("box") -- actor as base

        -- visible object
    if p.w then -- it's a box
        actor.l3d = l3dprimitive.newbox("box",p.w,p.h,p.d) 
    end
    if p.rad then -- it's a sphere
        actor.l3d = l3dprimitive.newsphere("box",p.rad) 
    end
    actor.l3d:linkinterface(actor)
    actor.l3d:rfLitSun(true)
    local r,g,b = UtilFunctions.color3hsv(p.hue,1,1)
    actor.l3d:color(r,g,b,1) -- hue is easier to set

        -- physical geometry
    if p.w then -- box geom
        actor.geom = dgeombox.new(p.w,p.h,p.d,globalspace)
    end 
    if p.rad then -- it's a sphere geom
        actor.geom = dgeomsphere.new(p.rad,globalspace)
    end
    actor.geom:surfaceid(p.surfaceid or 0)
    if p.fdir then -- it has a friction direction vector
        actor.geom:fdirnormal(unpack(p.fdir))
    end

        -- our body now
    actor.body = dbody.new()
    if p.w then -- mass for box body
        actor.body:massbox(p.mass or 1, p.w,p.h,p.d,true)
    end
    if p.rad then -- mass for sphere body
        actor.body:masssphere(p.mass or 1, p.rad,true)
    end
    actor:link(actor.body) -- link it with the actor
    actor.geom:body(actor.body) -- link it with the geom

        -- position and force setup
    actor.body:pos(p.x,p.y,p.z)
    actor.body:addforce(p.vx or 0,p.vy or 0,p.vz or 0)

        -- prevent Gargabe Collection
    objects[#objects+1] = actor
end

First we create a space to hold all objects. Since the scene consists only of dynamic objects (well, except for the ground), we have only one space.

As multiple objects are about to be created, I create a function that can create all these objects. It accepts a table as argument which contains all information of the body to be created, like if is a box or a sphere, initial force and so on.

Calling this function is fairly straight forward and simple:

-- create 6 boxes with different surfaceids
makebody{x=-2,y=0,z=0, w=.5,h=.5,d=.5, hue=.0, surfaceid=1}
makebody{x=-1,y=0,z=0, w=.5,h=.5,d=.5, hue=.2, surfaceid=2}
makebody{x= 0,y=0,z=0, w=.5,h=.5,d=.5, hue=.4, surfaceid=3}
makebody{x= 1,y=0,z=0, w=.5,h=.5,d=.5, hue=.6, surfaceid=4}
makebody{x= 2,y=0,z=0, w=.5,h=.5,d=.5, hue=.8, surfaceid=5, 
    fdir={0,1,0}} -- the violet box shall have an fdirnormal

-- create 6 spheres that push the objects
makebody{x=-2.3,y=4,z=0, rad=.25, hue=.5, surfaceid = 6, vy=-.05}
makebody{x=-1.3,y=4,z=0, rad=.25, hue=.5, surfaceid = 6, vy=-.05}
makebody{x=-0.3,y=4,z=0, rad=.25, hue=.5, surfaceid = 6, vy=-.05}
makebody{x= 0.7,y=4,z=0, rad=.25, hue=.5, surfaceid = 6, vy=-.05}
makebody{x= 1.7,y=4,z=0, rad=.25, hue=.5, surfaceid = 6, vy=-.05}

This is creating the 6 boxes and spheres that can be seen in the video. If no surface properties are defined, the balls will bump into the boxes and all will behave the same. To change this, we define the properties of the surface, and again, a function is used to simplify setting the material properties and melt all of them into a single table:

-- a simplified way how to set the surfaceproperties
function setsurfaceparam(p)
    -- p.combos: with which surfaceids does it map to this material?
    for i,with in ipairs(p.combos) do
        -- so if p.surfaceid collides with our surfaceid, 
        -- let's map it to p.matid
        dworld.surfacecombination(p.surfaceid,with,p.matid)
    end

    -- we don't want to use these features now, but 
    -- they are enabled per default:
    dworld.surfacebitsoftcfm(p.matid,false)
    dworld.surfacebitsofterp(p.matid,false)

    -- le't set the friction values (mu)
    dworld.surfacemu(p.matid,p.mu or 1000)
    -- mu2 makes only sense in combination with the fdir
    dworld.surfacemu2(p.matid,p.mu2 or 1000)
    -- mu2 must be enabled
    dworld.surfacebitmu2(p.matid,p.mu2 and true or false)


    -- this defines how to use the fdirnormal, we set it
    -- that way that it uses the fdirnormal of our id
    dworld.surfacebitfdir(p.matid,p.fdir and true or false)
    dworld.surfacefdirmixmode(p.matid,4)
    dworld.surfacefdirid(p.matid,p.surfaceid)
end

The function also takes only one argument which is called this way:

-- let's set the material properties
setsurfaceparam{surfaceid=1,matid=1, combos={6,0}, mu=0}
setsurfaceparam{surfaceid=2,matid=2, combos={6,0}, mu=.05}
setsurfaceparam{surfaceid=3,matid=3, combos={6,0}, mu=.1}
setsurfaceparam{surfaceid=5,matid=5, combos={6,0}, mu=0.001, 
    mu2=10.5, fdir=true}

Adding some gravity and a timer is shown in the script file and does not require much explanation (except maybe that the simulation starts if you press the spacebar ;))

For further information on what parameters can be set, take a look at the dworld class description. All functions that change the surface properties are starting with "surface" in their names.