bool Law2_ScGeom_MindlinPhys_MindlinDeresiewitz::go(shared_ptr<IGeom>& ig, shared_ptr<IPhys>& ip, Interaction* contact){ Body::id_t id1(contact->getId1()), id2(contact->getId2()); ScGeom* geom = static_cast<ScGeom*>(ig.get()); MindlinPhys* phys=static_cast<MindlinPhys*>(ip.get()); const Real uN=geom->penetrationDepth; if (uN<0) { if (neverErase) {phys->shearForce = phys->normalForce = Vector3r::Zero(); phys->kn=phys->ks=0; return true;} else {return false;} } // normal force Real Fn=phys->kno*pow(uN,3/2.); phys->normalForce=Fn*geom->normal; // exactly zero would not work with the shear formulation, and would give zero shear force anyway if(Fn==0) return true; //phys->kn=3./2.*phys->kno*std::pow(uN,0.5); // update stiffness, not needed // contact radius Real R=geom->radius1*geom->radius2/(geom->radius1+geom->radius2); phys->radius=pow(Fn*pow(R,3/2.)/phys->kno,1/3.); // shear force: transform, but keep the old value for now geom->rotate(phys->usTotal); //Vector3r usOld=phys->usTotal; //The variable set but not used Vector3r dUs=geom->shearIncrement(); phys->usTotal-=dUs; #if 0 Vector3r shearIncrement; shearIncrement=geom->shearIncrement(); Fs-=ks*shearIncrement; // Mohr-Coulomb slip Real maxFs2=pow(Fn,2)*pow(phys->tangensOfFrictionAngle,2); if(Fs.squaredNorm()>maxFs2) Fs*=sqrt(maxFs2)/Fs.norm(); #endif // apply forces Vector3r f=-phys->normalForce-phys->shearForce; scene->forces.addForce(id1,f); scene->forces.addForce(id2,-f); scene->forces.addTorque(id1,(geom->radius1-.5*geom->penetrationDepth)*geom->normal.cross(f)); scene->forces.addTorque(id2,(geom->radius2-.5*geom->penetrationDepth)*geom->normal.cross(f)); return true; }
bool Law2_ScGeom_MindlinPhys_HertzWithLinearShear::go(shared_ptr<IGeom>& ig, shared_ptr<IPhys>& ip, Interaction* contact){ Body::id_t id1(contact->getId1()), id2(contact->getId2()); ScGeom* geom = static_cast<ScGeom*>(ig.get()); MindlinPhys* phys=static_cast<MindlinPhys*>(ip.get()); const Real uN=geom->penetrationDepth; if (uN<0) { if (neverErase) {phys->shearForce = phys->normalForce = Vector3r::Zero(); phys->kn=phys->ks=0; return true;} else return false; } // normal force Real Fn=phys->kno*pow(uN,3/2.); phys->normalForce=Fn*geom->normal; //phys->kn=3./2.*phys->kno*std::pow(uN,0.5); // update stiffness, not needed // shear force Vector3r& Fs=geom->rotate(phys->shearForce); Real ks= nonLin>0 ? phys->kso*std::pow(uN,0.5) : phys->kso; Vector3r shearIncrement; if(nonLin>1){ State *de1=Body::byId(id1,scene)->state.get(), *de2=Body::byId(id2,scene)->state.get(); Vector3r shiftVel=scene->isPeriodic ? Vector3r(scene->cell->velGrad*scene->cell->hSize*contact->cellDist.cast<Real>()) : Vector3r::Zero(); Vector3r shift2 = scene->isPeriodic ? Vector3r(scene->cell->hSize*contact->cellDist.cast<Real>()): Vector3r::Zero(); Vector3r incidentV = geom->getIncidentVel(de1, de2, scene->dt, shift2, shiftVel, /*preventGranularRatcheting*/ nonLin>2 ); Vector3r incidentVn = geom->normal.dot(incidentV)*geom->normal; // contact normal velocity Vector3r incidentVs = incidentV-incidentVn; // contact shear velocity shearIncrement=incidentVs*scene->dt; } else { shearIncrement=geom->shearIncrement(); } Fs-=ks*shearIncrement; // Mohr-Coulomb slip Real maxFs2=pow(Fn,2)*pow(phys->tangensOfFrictionAngle,2); if(Fs.squaredNorm()>maxFs2) Fs*=sqrt(maxFs2)/Fs.norm(); // apply forces Vector3r f=-phys->normalForce-phys->shearForce; /* should be a reference returned by geom->rotate */ assert(phys->shearForce==Fs); scene->forces.addForce(id1,f); scene->forces.addForce(id2,-f); scene->forces.addTorque(id1,(geom->radius1-.5*geom->penetrationDepth)*geom->normal.cross(f)); scene->forces.addTorque(id2,(geom->radius2-.5*geom->penetrationDepth)*geom->normal.cross(f)); return true; }
void Law2_ScGeom_CFpmPhys_CohesiveFrictionalPM::go(shared_ptr<IGeom>& ig, shared_ptr<IPhys>& ip, Interaction* contact){ ScGeom* geom = static_cast<ScGeom*>(ig.get()); CFpmPhys* phys = static_cast<CFpmPhys*>(ip.get()); const int &id1 = contact->getId1(); const int &id2 = contact->getId2(); Body* b1 = Body::byId(id1,scene).get(); Body* b2 = Body::byId(id2,scene).get(); Real displN = geom->penetrationDepth; // NOTE: the sign for penetrationdepth is different from ScGeom and Dem3DofGeom: geom->penetrationDepth>0 when spheres interpenetrate Real Dtensile=phys->FnMax/phys->kn; Real Dsoftening = phys->strengthSoftening*Dtensile; /*to set the equilibrium distance between all cohesive elements when they first meet -> allows one to work with initial stress-free assembly*/ if ( contact->isFresh(scene) ) { phys->initD = displN; phys->normalForce = Vector3r::Zero(); phys->shearForce = Vector3r::Zero();} Real D = displN - phys->initD; // interparticular distance is computed depending on the equilibrium distance /* Determination of interaction */ if (D < 0){ //spheres do not touch if (!phys->isCohesive){ scene->interactions->requestErase(contact); return; } // destroy the interaction before calculation if ((phys->isCohesive) && (abs(D) > (Dtensile + Dsoftening))) { // spheres are bonded and the interacting distance is greater than the one allowed ny the defined cohesion phys->isCohesive=false; // update body state with the number of broken bonds CFpmState* st1=dynamic_cast<CFpmState*>(b1->state.get()); CFpmState* st2=dynamic_cast<CFpmState*>(b2->state.get()); st1->numBrokenCohesive+=1; st2->numBrokenCohesive+=1; //// the same thing but from ConcretePM //const shared_ptr<Body>& body1=Body::byId(contact->getId1(),scene), body2=Body::byId(contact->getId2(),scene); assert(body1); assert(body2); //const shared_ptr<CFpmState>& st1=YADE_PTR_CAST<CFpmState>(body1->state), st2=YADE_PTR_CAST<CFpmState>(body2->state); //{ boost::mutex::scoped_lock lock(st1->updateMutex); st1->numBrokenCohesive+=1; } //{ boost::mutex::scoped_lock lock(st2->updateMutex); st2->numBrokenCohesive+=1; } // end of update scene->interactions->requestErase(contact); return; } } /*NormalForce*/ Real Fn=0, Dsoft=0; if ((D < 0) && (abs(D) > Dtensile)) { //to take into account strength softening Dsoft = D+Dtensile; // Dsoft<0 for a negative value of Fn (attractive force) Fn = -(phys->FnMax+(phys->kn/phys->strengthSoftening)*Dsoft); // computes FnMax - FnSoftening } else { Fn = phys->kn*D; } phys->normalForce = Fn*geom->normal; // NOTE normal is position2-position1 - It is directed from particle1 to particle2 /*ShearForce*/ Vector3r& shearForce = phys->shearForce; // using scGeom function rotateAndGetShear State* st1 = Body::byId(id1,scene)->state.get(); State* st2 = Body::byId(id2,scene)->state.get(); geom->rotate(phys->shearForce); const Vector3r& dus = geom->shearIncrement(); //Linear elasticity giving "trial" shear force shearForce -= phys->ks*dus; // needed for the next timestep phys->prevNormal = geom->normal; /* Morh-Coulomb criterion */ Real maxFs = phys->FsMax + Fn*phys->tanFrictionAngle; if (shearForce.squaredNorm() > maxFs*maxFs){ shearForce*=maxFs/shearForce.norm(); // to fix the shear force to its yielding value } /* Apply forces */ Vector3r f = phys->normalForce + shearForce; // these lines to adapt to periodic boundary conditions (NOTE applyForceAtContactPoint computes torque induced by normal and shear force too) if (!scene->isPeriodic) applyForceAtContactPoint(f , geom->contactPoint , id2, st2->se3.position, id1, st1->se3.position); else { // in scg we do not wrap particles positions, hence "applyForceAtContactPoint" cannot be used when scene is periodic scene->forces.addForce(id1,-f); scene->forces.addForce(id2,f); scene->forces.addTorque(id1,(geom->radius1-0.5*geom->penetrationDepth)* geom->normal.cross(-f)); scene->forces.addTorque(id2,(geom->radius2-0.5*geom->penetrationDepth)* geom->normal.cross(-f)); } /* Moment Rotation Law */ // NOTE this part could probably be computed in ScGeom to avoid copy/paste multiplication !!! Quaternionr delta( b1->state->ori * phys->initialOrientation1.conjugate() *phys->initialOrientation2 * b2->state->ori.conjugate()); delta.normalize(); //relative orientation AngleAxisr aa(delta); // axis of rotation - this is the Moment direction UNIT vector; angle represents the power of resistant ELASTIC moment if(aa.angle() > Mathr::PI) aa.angle() -= Mathr::TWO_PI; // angle is between 0 and 2*pi, but should be between -pi and pi phys->cumulativeRotation = aa.angle(); //Find angle*axis. That's all. But first find angle about contact normal. Result is scalar. Axis is contact normal. Real angle_twist(aa.angle() * aa.axis().dot(geom->normal) ); //rotation about normal Vector3r axis_twist(angle_twist * geom->normal); Vector3r moment_twist(axis_twist * phys->kr); Vector3r axis_bending(aa.angle()*aa.axis() - axis_twist); //total rotation minus rotation about normal Vector3r moment_bending(axis_bending * phys->kr); Vector3r moment = moment_twist + moment_bending; Real MomentMax = phys->maxBend*std::fabs(phys->normalForce.norm()); Real scalarMoment = moment.norm(); /*Plastic moment */ if(scalarMoment > MomentMax) { Real ratio = 0; ratio *= MomentMax/scalarMoment; // to fix the moment to its yielding value moment *= ratio; moment_twist *= ratio; moment_bending *= ratio; } phys->moment_twist = moment_twist; phys->moment_bending = moment_bending; scene->forces.addTorque(id1,-moment); scene->forces.addTorque(id2, moment); }
bool Law2_ScGeom_JCFpmPhys_JointedCohesiveFrictionalPM::go(shared_ptr<IGeom>& ig, shared_ptr<IPhys>& ip, Interaction* contact){ const int &id1 = contact->getId1(); const int &id2 = contact->getId2(); ScGeom* geom = static_cast<ScGeom*>(ig.get()); JCFpmPhys* phys = static_cast<JCFpmPhys*>(ip.get()); Body* b1 = Body::byId(id1,scene).get(); Body* b2 = Body::byId(id2,scene).get(); Real Dtensile=phys->FnMax/phys->kn; string fileCracks = "cracks_"+Key+".txt"; string fileMoments = "moments_"+Key+".txt"; /// Defines the interparticular distance used for computation Real D = 0; /*this is for setting the equilibrium distance between all cohesive elements at the first contact detection*/ if ( contact->isFresh(scene) ) { phys->normalForce = Vector3r::Zero(); phys->shearForce = Vector3r::Zero(); if ((smoothJoint) && (phys->isOnJoint)) { phys->jointNormal = geom->normal.dot(phys->jointNormal)*phys->jointNormal; //to set the joint normal colinear with the interaction normal phys->jointNormal.normalize(); phys->initD = std::abs((b1->state->pos - b2->state->pos).dot(phys->jointNormal)); // to set the initial gap as the equilibrium gap } else { phys->initD = geom->penetrationDepth; } } if ( smoothJoint && phys->isOnJoint ) { if ( phys->more || ( phys-> jointCumulativeSliding > (2*min(geom->radius1,geom->radius2)) ) ) { if (!neverErase) return false; else { phys->shearForce = Vector3r::Zero(); phys->normalForce = Vector3r::Zero(); phys->isCohesive =0; phys->FnMax = 0; phys->FsMax = 0; return true; } } else { D = phys->initD - std::abs((b1->state->pos - b2->state->pos).dot(phys->jointNormal)); } } else { D = geom->penetrationDepth - phys->initD; } phys->crackJointAperture = D<0? -D : 0.; // for DFNFlow if (!phys->momentBroken && useStrainEnergy) phys->strainEnergy = 0.5*((pow(phys->normalForce.norm(),2)/phys->kn) + (pow(phys->shearForce.norm(),2)/phys->ks)); else if (!phys->momentBroken && !useStrainEnergy) computeKineticEnergy(phys, b1, b2); //Compute clustered acoustic emission events: if (recordMoments && !neverErase){ cerr << "Acoustic emissions algorithm requires neverErase=True, changing value from False to True" << endl; neverErase=true; } if (phys->momentBroken && recordMoments && !phys->momentCalculated){ if (phys->originalClusterEvent && !phys->computedCentroid) computeCentroid(phys); if (phys->originalClusterEvent) computeClusteredMoment(phys); if (phys->momentCalculated && phys->momentMagnitude!=0){ std::ofstream file (fileMoments.c_str(), !momentsFileExist ? std::ios::trunc : std::ios::app); if(file.tellp()==0){ file <<"i p0 p1 p2 moment numInts eventNum time beginTime"<<endl; } file << boost::lexical_cast<string> ( scene->iter )<<" "<< boost::lexical_cast<string> ( phys->momentCentroid[0] ) <<" "<< boost::lexical_cast<string> ( phys->momentCentroid[1] ) <<" "<< boost::lexical_cast<string> ( phys->momentCentroid[2] ) <<" "<< boost::lexical_cast<string> ( phys->momentMagnitude ) << " " << boost::lexical_cast<string> ( phys->clusterInts.size() ) << " " << boost::lexical_cast<string> ( phys->eventNumber ) << " " << boost::lexical_cast<string> (scene->time) << " " << boost::lexical_cast<string> (phys->eventBeginTime) << endl; momentsFileExist=true; } } /* Determination of interaction */ if (D < 0) { //tensile configuration if ( !phys->isCohesive) { if (!neverErase) return false; else { phys->shearForce = Vector3r::Zero(); phys->normalForce = Vector3r::Zero(); phys->isCohesive =0; phys->FnMax = 0; phys->FsMax = 0; return true; } } if ( phys->isCohesive && (phys->FnMax>0) && (std::abs(D)>Dtensile) ) { nbTensCracks++; phys->isCohesive = 0; phys->FnMax = 0; phys->FsMax = 0; /// Do we need both the following lines? phys->breakOccurred = true; // flag to trigger remesh for DFNFlowEngine phys->isBroken = true; // flag for DFNFlowEngine // update body state with the number of broken bonds -> do we really need that? JCFpmState* st1=dynamic_cast<JCFpmState*>(b1->state.get()); JCFpmState* st2=dynamic_cast<JCFpmState*>(b2->state.get()); st1->nbBrokenBonds++; st2->nbBrokenBonds++; st1->damageIndex+=1.0/st1->nbInitBonds; st2->damageIndex+=1.0/st2->nbInitBonds; phys->momentBroken = true; Real scalarNF=phys->normalForce.norm(); Real scalarSF=phys->shearForce.norm(); totalTensCracksE+=0.5*( ((scalarNF*scalarNF)/phys->kn) + ((scalarSF*scalarSF)/phys->ks) ); totalCracksSurface += phys->crossSection; if (recordCracks){ std::ofstream file (fileCracks.c_str(), !cracksFileExist ? std::ios::trunc : std::ios::app); if(file.tellp()==0){ file <<"iter time p0 p1 p2 type size norm0 norm1 norm2 nrg onJnt"<<endl; } Vector3r crackNormal=Vector3r::Zero(); if ((smoothJoint) && (phys->isOnJoint)) { crackNormal=phys->jointNormal; } else {crackNormal=geom->normal;} file << boost::lexical_cast<string> ( scene->iter ) << " " << boost::lexical_cast<string> ( scene->time ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[0] ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[1] ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[2] ) <<" "<< 1 <<" "<< boost::lexical_cast<string> ( 0.5*(geom->radius1+geom->radius2) ) <<" "<< boost::lexical_cast<string> ( crackNormal[0] ) <<" "<< boost::lexical_cast<string> ( crackNormal[1] ) <<" "<< boost::lexical_cast<string> ( crackNormal[2] ) <<" "<< boost::lexical_cast<string> ( 0.5*( ((scalarNF*scalarNF)/phys->kn) + ((scalarSF*scalarSF)/phys->ks) ) ) <<" "<< boost::lexical_cast<string> ( phys->isOnJoint ) << endl; } if (recordMoments && !phys->momentCalculated){ checkForCluster(phys, geom, b1, b2, contact); clusterInteractions(phys, contact); computeTemporalWindow(phys, b1, b2); } cracksFileExist=true; if (!neverErase) return false; else { phys->shearForce = Vector3r::Zero(); phys->normalForce = Vector3r::Zero(); return true; } } } /* NormalForce */ Real Fn = 0; Fn = phys->kn*D; /* ShearForce */ Vector3r& shearForce = phys->shearForce; Real jointSliding=0; if ((smoothJoint) && (phys->isOnJoint)) { /// incremental formulation (OK?) Vector3r relativeVelocity = (b2->state->vel - b1->state->vel); // angVel are not taken into account as particles on joint don't rotate ???? Vector3r slidingVelocity = relativeVelocity - phys->jointNormal.dot(relativeVelocity)*phys->jointNormal; Vector3r incrementalSliding = slidingVelocity*scene->dt; shearForce -= phys->ks*incrementalSliding; jointSliding = incrementalSliding.norm(); phys->jointCumulativeSliding += jointSliding; } else { shearForce = geom->rotate(phys->shearForce); const Vector3r& incrementalShear = geom->shearIncrement(); shearForce -= phys->ks*incrementalShear; } /* Mohr-Coulomb criterion */ Real maxFs = phys->FsMax + Fn*phys->tanFrictionAngle; Real scalarShearForce = shearForce.norm(); if (scalarShearForce > maxFs) { if (scalarShearForce != 0) shearForce*=maxFs/scalarShearForce; else shearForce=Vector3r::Zero(); if ((smoothJoint) && (phys->isOnJoint)) {phys->dilation=phys->jointCumulativeSliding*phys->tanDilationAngle-D; phys->initD+=(jointSliding*phys->tanDilationAngle);} // if (!phys->isCohesive) { // nbSlips++; // totalSlipE+=((1./phys->ks)*(trialForce-shearForce))/*plastic disp*/.dot(shearForce)/*active force*/; // // if ( (recordSlips) && (maxFs!=0) ) { // std::ofstream file (fileCracks.c_str(), !cracksFileExist ? std::ios::trunc : std::ios::app); // if(file.tellp()==0){ file <<"iter time p0 p1 p2 type size norm0 norm1 norm2 nrg"<<endl; } // Vector3r crackNormal=Vector3r::Zero(); // if ((smoothJoint) && (phys->isOnJoint)) { crackNormal=phys->jointNormal; } else {crackNormal=geom->normal;} // file << boost::lexical_cast<string> ( scene->iter ) <<" " << boost::lexical_cast<string> ( scene->time ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[0] ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[1] ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[2] ) <<" "<< 0 <<" "<< boost::lexical_cast<string> ( 0.5*(geom->radius1+geom->radius2) ) <<" "<< boost::lexical_cast<string> ( crackNormal[0] ) <<" "<< boost::lexical_cast<string> ( crackNormal[1] ) <<" "<< boost::lexical_cast<string> ( crackNormal[2] ) <<" "<< boost::lexical_cast<string> ( ((1./phys->ks)*(trialForce-shearForce)).dot(shearForce) ) << endl; // } // cracksFileExist=true; // } if ( phys->isCohesive ) { nbShearCracks++; phys->isCohesive = 0; phys->FnMax = 0; phys->FsMax = 0; /// Do we need both the following lines? phys->breakOccurred = true; // flag to trigger remesh for DFNFlowEngine phys->isBroken = true; // flag for DFNFlowEngine phys->momentBroken = true; // update body state with the number of broken bonds -> do we really need that? JCFpmState* st1=dynamic_cast<JCFpmState*>(b1->state.get()); JCFpmState* st2=dynamic_cast<JCFpmState*>(b2->state.get()); st1->nbBrokenBonds++; st2->nbBrokenBonds++; st1->damageIndex+=1.0/st1->nbInitBonds; st2->damageIndex+=1.0/st2->nbInitBonds; Real scalarNF=phys->normalForce.norm(); Real scalarSF=phys->shearForce.norm(); totalShearCracksE+=0.5*( ((scalarNF*scalarNF)/phys->kn) + ((scalarSF*scalarSF)/phys->ks) ); totalCracksSurface += phys->crossSection; if (recordCracks){ std::ofstream file (fileCracks.c_str(), !cracksFileExist ? std::ios::trunc : std::ios::app); if(file.tellp()==0){ file <<"iter time p0 p1 p2 type size norm0 norm1 norm2 nrg onJnt"<<endl; } Vector3r crackNormal=Vector3r::Zero(); if ((smoothJoint) && (phys->isOnJoint)) { crackNormal=phys->jointNormal; } else {crackNormal=geom->normal;} file << boost::lexical_cast<string> ( scene->iter ) << " " << boost::lexical_cast<string> ( scene->time ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[0] ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[1] ) <<" "<< boost::lexical_cast<string> ( geom->contactPoint[2] ) <<" "<< 2 <<" "<< boost::lexical_cast<string> ( 0.5*(geom->radius1+geom->radius2) ) <<" "<< boost::lexical_cast<string> ( crackNormal[0] ) <<" "<< boost::lexical_cast<string> ( crackNormal[1] ) <<" "<< boost::lexical_cast<string> ( crackNormal[2] ) <<" "<< boost::lexical_cast<string> ( 0.5*( ((scalarNF*scalarNF)/phys->kn) + ((scalarSF*scalarSF)/phys->ks) ) ) <<" "<< boost::lexical_cast<string> ( phys->isOnJoint ) << endl; } cracksFileExist=true; // // option 1: delete contact whatsoever (if in compression, it will be detected as a new contact at the next timestep -> actually, not necesarily because of the near neighbour interaction: there could be a gap between the bonded particles and thus a broken contact may not be frictional at the next timestep if the detection is done for strictly contacting particles...) -> to TEST // if (!neverErase) return false; // else { // phys->shearForce = Vector3r::Zero(); // phys->normalForce = Vector3r::Zero(); // return true; // } if (recordMoments && !phys->momentCalculated){ checkForCluster(phys, geom, b1, b2, contact); clusterInteractions(phys, contact); computeTemporalWindow(phys, b1, b2); } // option 2: delete contact if in tension // shearForce *= Fn*phys->tanFrictionAngle/scalarShearForce; // now or at the next timestep? should not be very different -> to TEST if ( D < 0 ) { // spheres do not touch if (!neverErase) return false; else { phys->shearForce = Vector3r::Zero(); phys->normalForce = Vector3r::Zero(); return true; } } } } /* Apply forces */ if ((smoothJoint) && (phys->isOnJoint)) { phys->normalForce = Fn*phys->jointNormal; } else { phys->normalForce = Fn*geom->normal; } Vector3r f = phys->normalForce + shearForce; /// applyForceAtContactPoint computes torque also and, for now, we don't want rotation for particles on joint (some errors in calculation due to specific geometry) //applyForceAtContactPoint(f, geom->contactPoint, I->getId2(), b2->state->pos, I->getId1(), b1->state->pos, scene); scene->forces.addForce (id1,-f); scene->forces.addForce (id2, f); // simple solution to avoid torque computation for particles interacting on a smooth joint if ( (phys->isOnJoint)&&(smoothJoint) ) return true; /// those lines are needed if rootBody->forces.addForce and rootBody->forces.addMoment are used instead of applyForceAtContactPoint -> NOTE need to check for accuracy!!! scene->forces.addTorque(id1,(geom->radius1-0.5*geom->penetrationDepth)* geom->normal.cross(-f)); scene->forces.addTorque(id2,(geom->radius2-0.5*geom->penetrationDepth)* geom->normal.cross(-f)); return true; }