Dynamic transform
From H3D.org
The use of haptics in an X3D scene can open up many possibilities. However, with the existing X3D nodes, if we were to create interaction that depends on motion information like the velocity, angular velocity, momentum and more, we should have to make the calculations ourselves. The DynamicTransform node in H3DAPI saves the developer from this inconvenience. DynamicTransform is an X3DGroupingNode that serves as a Shape container and provides the basic properties to define rigid body motion.
In this tutorial we will look at how we manipulate and use information from the fields of DynamicTransform to create a cube that responses to the forces applied through touch.
The cube
What we want to to with the following example is to have a cube that reacts to forces applied to it as in real life. We also want the cube to behave in a way such that it always returns to its original orientation and position when there are no forces acting on it.
<!!-- dynamic.x3d --> <Scene> <GlobalSettings> <HapticsOptions maxDistance="-1" /> </GlobalSettings>
First, we include a GobalSettings node into the scene. GobalSettings is a bindable node containing default settings for the scene. In this example, there is only one settings adjustment that we make - the maxDistance of HapticsOptions is set to -1. maxDistance specifies the maximum distance the proxy can be from a primitive in order for the primitive to be rendered haptically. We set the value to a negative number indicating that we want the haptics to be rendered at all times.
<!!-- dynamic.x3d --> <DynamicTransform DEF="DYN" mass="20" inertiaTensor="0.1 0 0 0 0.1 0 0 0 0.1"> <Shape> <Appearance> <Material diffuseColor="1 0 0"/> <FrictionalSurface dynamicFriction="0.6" staticFriction="0.2"/> </Appearance> <Box DEF="BOX" size="0.2 0.2 0.2" /> </Shape> </DynamicTransform>
We add the cube and its properties to the scene the way we normally do, but this time the Shape is added as a child to the DynamicTransform node. The mass field specifies the entire mass of the object and is set to 20kg.
<!!-- dynamic.x3d --> <PythonScript DEF="PS" url="dynamic.py" />
What is left to do now is to define the physics for the motion of the cube. This will be done in the Python script, inserted here in the scene with the PythonScript node.
The physics
There are two main aspects to consider when we define the behaviour of our cube: the linear forces and the angular forces. To return the cube to its original position we will apply on the cube the opposite of the current linear forces on the cube. To return the cube to its original orientation we will apply on the cube the opposite of the current angular forces on the cube. Our script to create this physics will thus calculate:
- the sum of linear forces on the cube, which comprises:
- a spring force that pulls the cube back to its origin
- a damping force that slows the cube
- the invert of the forces acting on the cube by the haptics device(s)
- the sum of angular forces on the cube, which comprises:
- an angular spring force that pulls the cube back to its original orientation
- an angular damping force that slows the cube
- the invert of the torques acting on the cube by the haptics device(s)
To do these calculations we will self-define the following fields in our script:
- linSpring, linDamper and invertForce to calculate and return the spring force, damping force and the forces from haptics device(s)
- sumForce that returns the total linear forces on the cube and is a sum of linSpring, linDamper and invertForce
- angSpring, angDamper and applyTorque to calculate and return the angular spring force, angular damper and the torques from haptics device(s)
- sumTorque that returns the total torques on the cube and is a sum of angSpring, angDamper and applyTorque
<!!-- dynamic.x3d --> <ROUTE fromNode="DYN" fromField="position" toNode="PS" toField="linSpring" /> <ROUTE fromNode="DYN" fromField="velocity" toNode="PS" toField="linDamper" /> <ROUTE fromNode="DYN" fromField="matrix" toNode="PS" toField="invertForce" /> <ROUTE fromNode="BOX" fromField="force" toNode="PS" toField="invertForce" /> <ROUTE fromNode="PS" fromField="sumForces" toNode="DYN" toField="force" />
We route the necessary properties from DynamicTransform and Box to our self-defined fields so that the calculations can be made:
- position will be used in calculation of spring force, and is thus routed to linSpring
- veloctiy will be used in calculation of damping force, and is thus routed to linDamper
- matrix (the transformation matrix of DynamicTransform) and force (the force applied by Box to haptics devices(s)) will be used in calculation of forces by haptics device, and is thus routed to invertForce
Finally, we want the DynamicTransform's force field to be updated with the value of sumForces, and so route sumForces to force.
<!!-- dynamic.x3d --> <ROUTE fromNode="DYN" fromField="orientation" toNode="PS" toField="angSpring" /> <ROUTE fromNode="DYN" fromField="angularVelocity" toNode="PS" toField="angDamper" /> <ROUTE fromNode="DYN" fromField="matrix" toNode="PS" toField="applyTorque" /> <ROUTE fromNode="BOX" fromField="force" toNode="PS" toField="applyTorque" /> <ROUTE fromNode="BOX" fromField="contactPoint" toNode="PS" toField="applyTorque" /> <ROUTE fromNode="PS" fromField="sumTorques" toNode="DYN" toField="torque" /> </Scene>
As with the calculation of linear forces, the properties of DynamicTransform and Box necessary for the calculation of total torque are routed to the respective self-defined fields. We then update the torque on the cube by routing our calculated sumTorque to DynamicTransform's torque.
And now we come to the calculations in Python:
#dynamic.py from H3DInterface import *
As usual we import the H3DInterface module so that all the H3D Python functions are available to us.
#dynamic.py # The LinSpring class generates a spring force that tries to keep the # nodes in the origin. # # routes_in[0] is the position of the DynamicTransform node. class LinSpring( SFVec3f ): def __init__( self, constant ): SFVec3f.__init__( self ) self.constant=constant def update( self, event ): dist = event.getValue() return -dist * self.constant linSpring = LinSpring( 100 ) # The LinDamper class generates a force to dampen the linear motion # of a DynamicTransform. # # routes_in[0] is the velocity of the DynamicTransform node. class LinDamper( SFVec3f ): def __init__( self, constant ): SFVec3f.__init__( self ) self.constant=constant def update( self, event ): vel = event.getValue() return -vel * self.constant linDamper = LinDamper( 10 )
The code above shows the definition of the linSpring and linDamper fields. As discussed, they calculate and return the linear spring and damping forces. The __init__ method is called automatically by Python right after the an instance of the field class has been created. In both classes, __init__ takes an argument that is a constant that can be set for the calculation of the forces. As can be seen, linSpring is constructed with the constant 100 and linDamper with 10.
#dynamic.py class InvertForce( TypedField( MFVec3f, (MFVec3f, SFMatrix4f ) ) ): def update( self, event ): try: forces, matrix = self.getRoutesIn() if( forces == event ): forces = event.getValue() forces_to_return = [] to_global = matrix.getValue().getRotationPart() for force in forces: forces_to_return.append( ( to_global * force ) * -1 ) return forces_to_return else: return [Vec3f( 0, 0, 0 ) ] except: return [Vec3f(0,0,0)] invertForce = InvertForce()
The InvertForce field class is responsible for gathering all the forces acting on the cube by the haptics device(s) and returning its total. It places the calculations in the try block to handle exceptions that may arise e.g. self.getRoutesIn() may not have two elements if there are no haptics devices detected in the system.
We then store the forces by the haptics device(s) and the transformation matrix in forces and matrix respectively. If the event sent to the InvertForce field comes from force, then we get the list of forces with the expression event.getValue().
The forces that we just obtained are defined in coordinates local to the Box node. We transform them to global coordinates by multiplying each of them with the rotation part of DynamicTransform's transformation matrix i.e. to_global * force. The forces routed to this field are actually the forces applied by Box to the haptics device(s). Since we want the inverse of these we multiply each by -1. The inverted forces are then appended to a new list of forces and returned.
Finally the invertForce field is created as an instance of InvertForce.
#dynamic.py class SumForces( TypedField( SFVec3f, (SFVec3f, SFVec3f, MFVec3f) ) ): def update( self, event ): f = Vec3f( 0, 0, 0 ); routes = getRoutesIn( self ) for r in routes: if isinstance( r, SFVec3f ): f = f + r.getValue(); elif isinstance( r, MFVec3f ): forces = r.getValue() for force in forces: f = f + force return f sumForces = SumForces()
The process in SumForces is simple, it will take input routes of either SFVec3f or MFVec3f type (since the input routes are all forces). It sums up all these inputs. If the input is of type MFVec3f, then it iterates the list to sum up the forces.
#dynamic.py linSpring.route( sumForces ) linDamper.route( sumForces ) invertForce.route( sumForces )
As we have seen, sumForce sums up the forces routed to it. The code excerpt above creates these routes.
We are now done with the linear forces calculations.
The fields pertaining to torques are similar to the fields we have gone through above. The only difference is the way the calculations are made.
#dynamic.py # The AngSpring class generates a torque that tries to keep the # DynamicTransform in the original position.. # # routes_in[0] is the orientation of the DynamicTransform node. class AngSpring( TypedField( SFVec3f, SFRotation ) ): def __init__( self, constant ): SFVec3f.__init__( self ) self.constant = constant def update( self, event ): orn = -event.getValue() diff = Rotation(1,0,0,0) * orn p = Vec3f( diff.x, diff.y, diff.z ) * diff.a * self.constant return p angSpring = AngSpring( 0.1 ) #1 # RotDamper dampens the angular motion of the dynamic. We route from # the dynamic's angularVelocity field to the dynamic's torque. # class AngDamper( SFVec3f ): def __init__( self, constant ): SFVec3f.__init__( self ) self.constant = constant def update( self, event ): angVel = event.getValue() return -angVel*self.constant angDamper = AngDamper( 0.1 ) # The ApplyTorque field calulates the torque applied to the Box with the # haptics device. # # routes_in[0] is the matrix field of the DynamicTransform # routes_in[1] is the force field of the Box node # routes_in[2] is the contactPoint field of the box node. class ApplyTorque( TypedField( SFVec3f, ( SFMatrix4f, MFVec3f, MFVec3f ) ) ): def update( self, event ): try: matrix, force, points = self.getRoutesIn() mypoints = points.getValue() to_global = matrix.getValue() temp_force = force.getValue() torque = Vec3f( 0, 0, 0 ) fulcrum = to_global * torque for i in range( len(mypoints) ): touch_point = to_global * mypoints[i] torque = torque + (fulcrum - touch_point) % ( to_global.getRotationPart() * temp_force[i] ) return torque except: return Vec3f( 0, 0, 0 ) # The SumTorques class just sums all SFVec3f fields that are routed to it. class SumTorques( SFVec3f ): def update( self, event ): f = Vec3f( 0, 0, 0 ); routes_in = getRoutesIn( self ) for r in routes_in: f = f + r.getValue(); return f sumTorques = SumTorques() applyTorque = ApplyTorque() angSpring.route( sumTorques ) angDamper.route( sumTorques ) applyTorque.route( sumTorques )