Advanced HAPI programming

From H3D.org

Jump to: navigation, search

Contents

Adding support for new Haptics Device

To add support for a new Haptics Device simply subclass HAPIHapticsDevice. There are a four functions that should be overridden in the new class. These are:

virtual bool initHapticsDevice( int _thread_frequency = 1000 ) = 0;
virtual bool releaseHapticsDevice() = 0;
virtual void updateDeviceValues( DeviceValues &dv, HAPITime dt )
virtual void sendOutput( DeviceOutput &dv, HAPITime dt ) = 0;


In order for AnyHapticsDevice to be able to detect the new haptics device the class has to be added to a static instance of the class HAPIHapticsDevice::HapticsDeviceRegistration.

For more information about what these classes do check the source code and/or the doxygen documentation for the HAPIHapticsDevice class (implemented in HAPIHapticsDevice.h and HAPIHapticsDevice.cpp). For example implementations take a look at the other classes inheriting from HAPIHapticsDevice.

Custom made Haptics Renderer

To implement a new haptics renderer subclass from HAPIHapticsRenderer. The function that must be overridden for the renderer to actually do something is.

virtual HAPIForceEffect::EffectOutput renderHapticsOneStep( HAPIHapticsDevice *hd,
                                                            const HapticShapeVector &shapes )

This function will be given a haptics device and a group of shapes as input and should return an EffectOutput struct which contains the generated force and torque. There are a few more functions that might be interesting to override for intialization and deinitialization of the renderer.

virtual void initRenderer( HAPIHapticsDevice *hd )
virtual void releaseRenderer( HAPIHapticsDevice *hd )

Sometimes there might be a need for processing the shapes before sending them to the renderer. If this is required then override this function:

void preProcessShapes( HAPIHapticsDevice *hd, const HapticShapeVector &shapes )

If the haptics renderer use some kind of proxy the new renderer class should subclass HAPIProxyBasedRenderer. This adds one more function that needs to be overridden:

virtual Vec3 getProxyPosition() = 0;

For more information about what these classes do check the source code and/or the doxygen documentation for the HAPIHapticsRenderer and HAPIProxyBasedRenderer classes. For example implementations take a look at the other classes inheriting from the base classes.

Custom made Haptics Surface

To create a custom haptics surface create a new class which subclasses HAPISurfaceObject and override the functions:

virtual void getProxyMovement( ContactInfo &contact_info );
virtual void getForces( ContactInfo &contact_info );

Where ContactInfo is a struct that contains information about the shape of contact, and about the contact itself. The first function should set the movement of the proxy and the second function should be used to calculate the force of the surface.

For all surfaces which has different depths in different parts of the surface the HAPIVariableDepthSurface class should be subclassed. When subclassing this class there is no need to override getProxyMovement and getForces unless a different behaviour than the default one for HAPIVariableDepthSurface is required. Instead a static function should be provided when calling the constructor of HAPIVariableDepthSurface. The function should be of the form
static HAPIFloat myFunctionName( const Vec2 &local_point, void *data )

For more information about what these classes do check the source code and/or the doxygen documentation for the HAPISurfaceObject and HAPIVariableDepthSurface classes. For example implementations take a look at the other classes inheriting from these classes and also the tutorial below.

SinusTextureCoordinateSurface (tutorial)

This section contains a small tutorial on how to create your own surfaces in HAPI. The surface created will depend on the texture coordinates at the point of contact between the haptics device and the haptic shape. The s coordinate will modify the magnitude and the t coordinate will modify the frequency of a sinus function that animates the force that acts on the haptics device upon contact. The strength of the force is also dependent on a stiffness variable. Higher stiffness means higher average force.

The tutorial consists of an explanation of the code needed to implement this custom haptics surface. We will start by the declarations in the header-file. First we need to include the needed files and declare the class. For convenience we will declare the class in the HAPI namespace.

// Include HAPI files.
#include <HAPI/HAPISurfaceObject.h>
 
namespace HAPI {
  class SinusTextureCoordinateSurface : public HAPISurfaceObject {
  public:

Next comes the constructor, which only takes one argument since the stiffness is the only property the user of this surface is allowed to modify.

/// Constructor
    SinusTextureCoordinateSurface( HAPIFloat _stiffness = 350 );
 

The two virtual functions that it is important to override comes next.

/// Determines the proxy movement of a contact. In this case just move
    /// proxy to the point on the surface closest to the probe. This
    /// corresponds to a surface without friction.
    virtual void getProxyMovement( ContactInfo &contact_info );
 
    /// Determines the force generated at a contact. For this surface
    /// the force will be modeled as a spring force towards the proxy.
    /// The strength of the force will be varying with a magnitude and
    /// frequency that depends on the texture coordinate at the contact point.
    virtual void getForces( ContactInfo &contact_info );

Finally we declare the variables of this class and put in the end brackets. The stiffness property is public since we want the user to have the option of changing the stiffness after creating and instance of the surface.

/// The stiffness of the surface.
    HAPIFloat stiffness;
 
  protected:
    /// The center of the wave that travels in the positive direction of
    /// the texture coordinates s-dimension.
    HAPIFloat wave_center;
 
    /// The lowest angular velocity for the wave defined by wave_center.
    HAPIFloat lowest_angular_velocity;
  };
}

Now lets look at the definition of the constructor and the two virtual functions. This code is assumed to be placed in the source file. First we add the include files and the using namespace HAPI-statement for convenient access to HAPI classes. Next we define the constructor which is straightforward.

// Name of header file in which the class declaration exists.
#include "SinusTextureCoordinateSurface.h"
// The code access properties of the haptics device in use and therefore need
// to include this file.
#include <HAPI/HAPIHapticsDevice.h>
 
using namespace HAPI;
 
SinusTextureCoordinateSurface::SinusTextureCoordinateSurface( HAPIFloat _stiffness ) :
HAPISurfaceObject( true ),
stiffness( _stiffness ),
wave_center(0),
lowest_angular_velocity( 0.01 ){
}

Next we define the getProxyMovement function. The purpose of this function is to set the local proxy movement in the given ContactInfo struct. This will tell the haptic renderer where it should try to move the proxy. Since we want a perfectly smooth surface we simply set the local proxy movement to the closest point to the probe. The local probe position gives us the position of the probe in local coordinates and since the origin of the local coordinate system is at the contact point the x and z components of this position is the position we want the proxy to move to.

void SinusTextureCoordinateSurface::getProxyMovement(
  ContactInfo &contact_info ) {
  // No friction at all.
  Vec3 local_probe = contact_info.localProbePosition();
  contact_info.setLocalProxyMovement( Vec2( local_probe.x , local_probe.z ) );
}

Finally we have the definition of the getForces function. This is the function that calculates the force this surface generates for the contact. The force is sent to the haptics renderer by setting the force property of the given ContactInfo struct. That property is set by calling one of the functions named setLocalForce and setGlobalForce. When the getForces function is called the globalOrigin() function of the given ContactInfo struct contains the new proxy position. In our case we use this to calculate the force direction. The code for the getForces functions looks like this:

void SinusTextureCoordinateSurface::getForces( ContactInfo &contact_info ) {
  // Get the texture coordinate at the contact point.
  const Vec3 contact_tex_coord = contact_info.contactPointTexCoord();
 
  // Use the t component of the texture coordinate to calculate an
  // angular velocity. The t component corresponds to the y part of the
  // contact_tex_coord.
  HAPIFloat angular_velocity = 5 * 
    (contact_tex_coord.y > lowest_angular_velocity ?
     contact_tex_coord.y : lowest_angular_velocity);
 
  // Calculate the new position of the wave_center. The wave_center
  // is used as input to the sine function and is the cause of the
  // force animation.
  wave_center += 0.001 * angular_velocity;
  while( wave_center > 1 )
    wave_center -= 1;
 
  // Calculate the vector used for the direction of the force.
  Vec3 probe_to_origin = 
    contact_info.globalOrigin() - contact_info.globalProbePosition();
 
  Vec3 n_probe_to_origin = probe_to_origin;
  n_probe_to_origin.normalizeSafe();
  // Calculate the force.
  // The constant factor 0.1 is there to never scale the force to 0.
  // The s component of the texture coordinate is used to scale the
  // amplitude of the since function.
  contact_info.setGlobalForce(  probe_to_origin * stiffness *
    ( 0.1 + 0.9 * contact_tex_coord.x *
      H3DUtil::H3DSin(wave_center * H3DUtil::Constants::pi ) ) );
}

This is all the code needed to create a custom made haptics surface in HAPI. To use this surface simply modify the SurfaceExample distributed with HAPI. Simply replace the name FrictionSurface with SinusTextureCoordinateSurface on line 55 of SurfaceDemo.cpp. Remember to include the code for the SinusTextureCoordinateSurface class, for example by including the header file with the class declaration.

Custom made Haptics Shape

To create a custom haptics shape subclass HAPIHapticShape and override the functions:

virtual void closestPointOnShape( const Vec3 &p, Vec3 &cp, Vec3 &n, Vec3 &tc ) = 0;
virtual bool lineIntersectShape( const Vec3 &from,
                                 const Vec3 &to,
                                 Collision::IntersectionInfo &result,
                                 Collision::FaceType face ) = 0;
virtual bool movingSphereIntersectShape( HAPIFloat radius, const Vec3 &from, const Vec3 &to ) = 0;
virtual void getConstraintsOfShape( const Vec3 &point,
                                    Constraints &constraints,
                                    Collision::FaceType face = Collision::FRONT_AND_BACK,
                                    HAPIFloat radius = -1 ) = 0;
virtual void getTangentSpaceMatrixShape( const Vec3 &point, Matrix4 &result_mtx ) = 0;
virtual void glRenderShape() = 0;

The first four of these functions are basically collision detection algorithms. The fifth is used to transform from texture space to vertex space. The last one is only used when HAVE_OPENGL is defined in HAPI.h and is needed in some cases. There are more virtual functions that could be overridden but the listed ones are the the ones required.

For more information about what these classes do check the source code and/or the doxygen documentation for the HAPIHapticsShape. For example implementations take a look at the source code for other classes inheriting from that class and also check out the tutorial below.

HapticTetrahedron (tutorial)

The following tutorial consists of going through the code on how to create a haptic shape that is a regular tetrahedron.

First comes the include files and the declaration of the class in the HAPI namespace. The class is declared in the HAPI namespace for convenience sake. Apart from that there are nothing special about this part of the code.

#include <HAPI/HAPIHapticShape.h>
 
namespace HAPI {
 
  class HapticTetrahedron: public HAPIHapticShape {
  public:
 

Next we declare the constructor. If this declaration is compared with the declaration of the constructor for HAPIHapticsShape there is only one extra argument which is needed to define the shape. This indicates of course that we intend to send all the arguments apart from the first to the base class constructor.

/// Constructor.
    /// \param _edge_length is the length of the sides in the triangles
    /// creating the regular tetrahedron.
    HapticTetrahedron( HAPIFloat edge_length,
                       const Matrix4 &_transform,
                       HAPISurfaceObject *_surface,
                       Collision::FaceType _touchable_face = 
                       Collision::FRONT_AND_BACK,
                       void *_userdata = NULL,
                       int _shape_id = -1,
                       void (*_clean_up_func)( void * ) = 0 );

This next part is included for optimization. The function nrTriangles is part of HAPIGLShape which HAPIHapticsShape inherit from. The purpose of the nrTriangles is pure optimization features when using the FeedbackBufferCollector class. Since the HAPIGLShape class will not be defined unless HAVE_OPENGL is defined in HAPI.h we put the definition of nrTriangles inside an ifdef block.

#ifdef HAVE_OPENGL
    /// An upper bound on how many triangles are renderered.
    virtual int nrTriangles() {
      return 6;
    }
#endif

Next we declare the virtual classes that must be overridden, these functions are the ones listed in the section above. For all the collision detection functions the input arguments are given in local coordinates, the result should also be in local coordinates. Note that one of the functions is within a preprocessor definition statement for the same reasons as the nrTriangles function.

protected:
    /// Detect collision between a line segment and the object.
    /// \param from The start of the line segment(in local coords).
    /// \param to The end of the line segment(in local coords).
    /// \param result Contains info about closest intersection, if 
    /// line intersects object(in local coords).
    /// \param face The sides of the object that can be intersected. E.g.
    /// if FRONT, intersections will be reported only if they occur from
    /// the front side, i.e. the side in which the normal points. 
    /// \returns true if intersected, false otherwise.
    virtual bool lineIntersectShape( const Vec3 &from, 
                                     const Vec3 &to,
                                     Collision::IntersectionInfo &result,
                                     Collision::FaceType face = 
                                     Collision::FRONT_AND_BACK); 
 
    /// Get the closest point and normal on the object to the given point p.
    /// \param p The point to find the closest point to (in local coords).
    /// \param cp Return parameter for closest point (in local coords).
    /// \param n Return parameter for normal at closest point
    /// (in local coords).
    /// \param tc Return paramater for texture coordinate at closest point.
    virtual void closestPointOnShape( const Vec3 &p, Vec3 &cp, 
                                      Vec3 &n, Vec3 &tc );
 
    /// Detect collision between a moving sphere and the object.
    /// \param radius The radius of the sphere
    /// \param from The start position of the sphere
    /// \param to The end position of the sphere.
    /// \returns true if intersected, false otherwise.
    virtual bool movingSphereIntersectShape( HAPIFloat radius,
                                             const Vec3 &from, 
                                             const Vec3 &to );
 
    /// Get constraint planes of the shape. A proxy of a haptics renderer
    /// will always stay above any constraints added.
    ///
    /// \param point Point to constrain(in local coords).
    /// \param constraints Where to add the constraints.
    /// \param face Determines which faces of the shape will be seen as
    /// constraining.
    /// \param radius Only add constraints within this radius. If set to -1
    /// all constraints will be added.
    virtual void getConstraintsOfShape( const Vec3 &point,
                                        Constraints &constraints,
                                        Collision::FaceType face = 
                                        Collision::FRONT_AND_BACK,
                                        HAPIFloat radius = -1 );
 
    /// Calculates a matrix transforming a vector from local space of the shape
    /// to texture space of the shape.
    /// \param point The point at which to find the tangent vectors
    /// in local coordinates
    /// \param result_mtx Stores the calculated matrix
    virtual void getTangentSpaceMatrixShape( const Vec3 &point,
                                             Matrix4 &result_mtx );
 
#ifdef HAVE_OPENGL
    /// Render a graphical representation of the shape using OpenGL. This
    /// is used by the OpenHapticsRenderer when using feedback or depth
    /// buffer shapes. The rendering should be done in local space of the 
    /// shape, i.e. ignoring the transform matrix in the shape.
    virtual void glRenderShape();
#endif

Only one more declaration is needed. A vector containing instances of the class Collision::Triangles which will be used in order to implement the collision detection functions in the simplest (although maybe not the most efficient) way. After this the class declaration is finished with the brackets.

// Will store the triangles that defines the tetrahedron.
    vector< Collision::Triangle > tetrahedron_triangles;
  };
}

Now it is time to look at the definition of the different functions. Starting with the constructor. The definitions are assumed to be placed in a source file. The purpose of the constructor is to calculate the triangles to be stored in the variable tetrahedron_triangles. These triangles are the four triangles that defines the tetrahedron.

// Include the header file with the HapticTetrahedron class declaration.
#include "HapticTetrahedron.h"
// Include header with PlaneConstraint class declaration. Needed by
// getConstraints function.
#include <HAPI/PlaneConstraint.h>
 
using namespace HAPI;
 
HapticTetrahedron::HapticTetrahedron( HAPIFloat edge_length,
                                      const Matrix4 &_transform,
                                      HAPISurfaceObject *_surface,
                                      Collision::FaceType _touchable_face,
                                      void *_userdata,
                                      int _shape_id,
                                      void (*_clean_up_func)( void * ) ):
      HAPIHapticShape( _transform, _surface, _touchable_face, _userdata,
                       _shape_id, _clean_up_func ) {
  // Calculate needed to define the triangle vertices. See information about
  // a regular tetrahedron and equilateral triangles.
  HAPIFloat height = edge_length * H3DUtil::H3DSqrt( 2.0 / 3.0 );
  HAPIFloat edge_length_half = edge_length / 2;
  HAPIFloat base_triangle_half_height =
    edge_length_half * H3DUtil::H3DSqrt( 3.0 ) / 2;
 
  // Calculate the base vertices.
  vector< Vec3 > vertices;
  vertices.push_back( Vec3( 0, 0, -base_triangle_half_height ) );
  vertices.push_back( Vec3( -edge_length_half, 0, base_triangle_half_height ));
  vertices.push_back( Vec3( edge_length_half , 0, base_triangle_half_height ));
 
  // Calculate the z component of the centroid in order to know where the
  // fourth vertex should be positioned.
  HAPIFloat centroid_z = 0;
  for( unsigned int i = 0; i < vertices.size(); i++ )
    centroid_z += vertices[i].z;
  centroid_z = centroid_z / 3;
 
  vertices.push_back( Vec3( 0, height, centroid_z ) );
 
  // Specify the triangles the tetrahedron consists of. The triangles
  // are specified so that the normal of the triangles will be out from
  // the tetrahedron. This will make calculations in the getConstraint
  // function more straight forward.
  // Base triangle
  tetrahedron_triangles.push_back(
    Collision::Triangle( vertices[0],
                         vertices[2],
                         vertices[1],
                         Vec3( 0.5, 0, 1 ),
                         Vec3( 1, 0, 0 ),
                         Vec3( 0, 0, 0 ) ) );
 
  // Side triangles
  tetrahedron_triangles.push_back(
    Collision::Triangle( vertices[0],
                         vertices[1],
                         vertices[3],
                         Vec3( 0.5, 0, 1 ),
                         Vec3( 0, 0, 0 ),
                         Vec3( 0.5, 1, 0.5 ) ) );
 
  tetrahedron_triangles.push_back(
    Collision::Triangle( vertices[1],
                         vertices[2],
                         vertices[3],
                         Vec3( 0, 0, 0 ),
                         Vec3( 1, 0, 0 ),
                         Vec3( 0.5, 1, 0.5 ) ) );
 
  tetrahedron_triangles.push_back(
    Collision::Triangle( vertices[2],
                         vertices[0],
                         vertices[3],
                         Vec3( 1, 0, 0 ),
                         Vec3( 0.5, 0, 1 ),
                         Vec3( 0.5, 1, 0.5 ) ) );
}

Next comes the definition of the lineIntersectShape and closestPointOnShape function. They will call the corresponding function for all triangles in the tetrahedron_triangles variable and return the result closest to the original point.

// Returns the closest intersection on the shape.
bool HapticTetrahedron::lineIntersectShape( const Vec3 &from, 
                                        const Vec3 &to,
                                        Collision::IntersectionInfo &result,
                                        Collision::FaceType face ) {
  bool intersection = false;
  HAPIFloat closest_intersection_t;
  for( unsigned int i = 0; i < tetrahedron_triangles.size(); i++ ) {
    Collision::IntersectionInfo temp_ii;
    if( tetrahedron_triangles[i].lineIntersect( from, to, temp_ii, face ) ) {
      if( intersection ) {
        if( temp_ii.t < closest_intersection_t ) {
          closest_intersection_t = temp_ii.t;
          result = temp_ii;
        }
      } else {
        intersection = true;
        result = temp_ii;
        closest_intersection_t = temp_ii.t;
      }
      // The intersection can not be closer than this, return right away.
      if( intersection && closest_intersection_t < Constants::epsilon )
        return true;
    }
  }
  return intersection;
}
 
void HapticTetrahedron::closestPointOnShape( const Vec3 &p, Vec3 &cp, 
                                           Vec3 &n, Vec3 &tc ) {
  HAPIFloat smallest_dist_sqr;
  for( unsigned int i = 0; i < tetrahedron_triangles.size(); i++ ) {
    Vec3 tmp_cp, tmp_n, tmp_tc;
    if( i != 0 ) {
      tetrahedron_triangles[i].closestPoint( p, tmp_cp, tmp_n, tmp_tc );
      HAPIFloat tmp_smallest_dist_sqr = (p - tmp_cp).lengthSqr();
      if( tmp_smallest_dist_sqr < smallest_dist_sqr ) {
        cp = tmp_cp;
        n = tmp_n;
        tc = tmp_tc;
        smallest_dist_sqr = tmp_smallest_dist_sqr;
      }
    } else {
      tetrahedron_triangles[i].closestPoint( p, cp, n, tc );
      smallest_dist_sqr = (p - cp).lengthSqr();
    }
    // The closest point can not be closer than this, return right away.
    if( smallest_dist_sqr <= Constants::epsilon )
        return;
  }
}

The movingSphereIntersectShape function that comes next does not need the point of intersection between the shape and the moving sphere. The only thing that is interesting is if there is an intersection, therefore this function is, in our case, very simple-looking. It will return as soon as it detects intersection with any of the triangles, if no intersection is detected, return false.

bool HapticTetrahedron::movingSphereIntersectShape( HAPIFloat radius,
                                                const Vec3 &from, 
                                                const Vec3 &to ) {
  for( unsigned int i = 0; i < tetrahedron_triangles.size(); i++ ) {
    if( tetrahedron_triangles[i].movingSphereIntersect( radius, from, to ) ) {
      return true;
    }
  }
  return false;
}

The function getConstraintsOfShape is important if it is desired that the RuspiniRenderer functions properly since it depends heavily on this function. In the future there might be other renderers needing this function to. The function should return all constraints (as define in the class PlaneConstraint) that is needed. In our case it should be sufficient with giving one constraint if the point to constrain is outside the tetrahedron. However, when that point is inside the tetrahedron constraints from all four triangles must be given. Note that it is important to set the haptic_shape variable of the contraints to point to the instance of this class.

void HapticTetrahedron::getConstraintsOfShape( const Vec3 &point,
                                           Constraints &constraints,
                                           Collision::FaceType face,
                                           HAPIFloat radius ) {
  // Point is inside tetrahedron if the point is below all triangle planes.
  bool is_inside = false;
  if( tetrahedron_triangles[0].normal * point < 0 ) {
    is_inside = true;
    for( unsigned int i = 1; i < tetrahedron_triangles.size(); i++ )
      if( tetrahedron_triangles[i].normal *
          (point - tetrahedron_triangles[i].a) >= 0 ) {
        is_inside = false;
        break;
      }
  }
 
  if( is_inside ) {
    // If the collision face is front it means collision only from outside
    // which means that if we get here there are no constraints.
    if( face == Collision::FRONT )
      return;
 
    unsigned int size = constraints.size();
    for( unsigned int i = 0; i < tetrahedron_triangles.size(); i++ ) {
      tetrahedron_triangles[i].getConstraints( point, constraints, face, radius );
      if( size < constraints.size() ) {
        constraints.back().haptic_shape.reset( this );
        size++;
      }
    }
 
  } else {
    // Only return the closest constraint since point is outside tetrahedron.
    PlaneConstraint closest_constraint;
    HAPIFloat smallest_dist_sqr;
    bool found_constraint = false;
 
    for( unsigned int i = 0; i < tetrahedron_triangles.size(); i++ ) {
      Constraints temp_constraints;
      tetrahedron_triangles[i].getConstraints( point,
                                               temp_constraints,
                                               face,
                                               radius );
      if( !found_constraint ) {
        if( !temp_constraints.empty() ) {
          closest_constraint = temp_constraints.back();
          smallest_dist_sqr = (closest_constraint.point - point).lengthSqr();
          found_constraint = true;
        }
      } else {
        if( !temp_constraints.empty() ) {
          HAPIFloat temp_smallest_dist_sqr =
            (temp_constraints.back().point - point).lengthSqr();
          if( temp_smallest_dist_sqr < smallest_dist_sqr ) {
            closest_constraint = temp_constraints.back();
            smallest_dist_sqr = temp_smallest_dist_sqr;
          }
        }
      }
 
      if( found_constraint && smallest_dist_sqr < Constants::epsilon )
        break;
    }
 
    if( found_constraint ) {
      closest_constraint.haptic_shape.reset( this );
      constraints.push_back( closest_constraint );
    }
  }
}

Now there are only two functions left which we need to define. The first of them is very simple since a call to the glRenderShape function should render all triangles we simply call the render function of the containing triangles. The last function to complete the implementation of this new shape is a getTangentMatrixShape. Without this function the DepthMapSurface will not be possible to use properly with this shape. Since we are lazy we did not even check if it is possible to calculate such a matrix in a very simple way for a tetrahedron, instead we once again rely on the functions implemented for the containing Triangle class. In order to do this properly we need to call the getTangentSpaceMatrix function of the triangle closest to the point on the surface ( the point should actually be in one of the triangles, but we can not be completely sure about that ). Anyways, the code for the last two functions look like this:

#ifdef HAVE_OPENGL
void HapticTetrahedron::glRenderShape() {
  for( unsigned int i = 0; i < tetrahedron_triangles.size(); i++ ) {
    tetrahedron_triangles[i].render();
  }
}
#endif
 
void HapticTetrahedron::getTangentSpaceMatrixShape( const Vec3 &point,
                                                Matrix4 &result_mtx ) {
  unsigned int closest_triangle = 0;
  Vec3 temp_cp, temp_n;
  tetrahedron_triangles[0].closestPoint( point, temp_cp, temp_n, temp_n );
  HAPIFloat distance = ( temp_cp - point).lengthSqr();
  HAPIFloat temp_distance;
  for( unsigned int i = 1; i < tetrahedron_triangles.size(); i++ ) {
    tetrahedron_triangles[i].closestPoint( point, temp_cp, temp_n, temp_n );
    temp_distance = (temp_cp - point).lengthSqr();
    if( temp_distance < distance ) {
      closest_triangle = i;
      distance = temp_distance;
    }
  }
  tetrahedron_triangles[closest_triangle].
    getTangentSpaceMatrix( point, result_mtx );
}

As can be seen it requires quite a bit of code to implement a new shape, this is of course due to the collision detection functions. It might be that the code suggested here could be optimized quite much but we leave that as an exercise to the reader of this tutorial. In order to test this code modify the SurfaceExample distributed with HAPI. Replace the HapticSphere class with the HapticTetrahedron.

Custom made Haptics Force Effect

To create a custom haptics force effect simply subclass from HAPIForceEffect and override the following function:

EffectOutput calculateForces( const EffectInput &input ) = 0;

EffectInput contains information about the haptics device and the time passed since last haptics loop. The returned EffectOutput struct should contains force and torque calculated by the calculateForces function.

For more information about what this class do check the source code and/or the doxygen documentation for the HAPIForceEffect . For example implementations take a look at the classes inheriting from HAPIForceEffect and for a tutorial see next section.

InvertedHapticSpring (tutorial)

In this tutorial we will create a new haptics force effect which tries to push the haptics device away from a point. The calculated force will be 0 when the position of the haptics device is a certain distance from the point and increase as the distance between the position of the haptics device and the spring decreases. The force will be modeled as a spring model in the same way as for the HapticSpring class.

I will go through the source code line by line and explain what each part does. We start by declaring the class, in order to do that we need some files included from HAPI and to make things easier we declare the class in the HAPI namespace:

// Base class include
#include <HAPI/HAPIForceEffect.h>
// Needed by the calculateForces function
#include <HAPI/HAPIHapticsDevice.h>
 
namespace HAPI {
 
  class InvertedHapticSpring: public HAPIForceEffect {
  public:
 

Next comes the constructor, it takes three arguments and use these to set the internal variables which are used to define the behavior of the force effect.

/// Constructor
    InvertedHapticSpring( const Vec3 &_position,
                          HAPIFloat _spring_constant,
                          HAPIFloat _influence_radius ):
      position( _position ),
      spring_constant( _spring_constant ),
      influence_radius( _influence_radius ) {}

The next part in the code is very important, without overriding the virtual function "calculateForces" the new force effect class will not do anything when added to the haptics loop. Its only purpose is to calculate a force (and torque for 6-dof haptic rendering) and return a struct containing this force.

/// The force of the EffectOutput will be a force away from the position of
    /// the spring.
    EffectOutput virtual calculateForces( const EffectInput &input ) {
      Vec3 distance_vector = input.hd->getPosition() - position;
      HAPIFloat distance = distance_vector.length();
      if( distance < Constants::epsilon ) {
        // This case is higly unlikely but theoretically possible and
        // is therefore considered. Just push the device in some direction.
        distance = 1e-5;
      }
 
      if( distance < influence_radius )
        return EffectOutput( distance_vector * 
                             (influence_radius - distance) *
                             spring_constant / distance );
      else return EffectOutput( Vec3() );
    }

Lastly we declare the internal variables and put the end brackets.

protected:
    Vec3 position;
    HAPIFloat spring_constant;
    HAPIFloat influence_radius;
  };
}

If the code above is pasted into the code for SpringExample distributed with HAPI the new class InvertedHapticSpring can be used instead of HapticSpring. To test this change line 83-84 of SpringExample.cpp to the code below and recompile.

InvertedHapticSpring *spring_effect = new InvertedHapticSpring( Vec3( x, y, z ),
                                                                spring_constant,
                                                                0.05 );
Personal tools
go to