void PxSingleActor::sweepTest( MatrixF *mat ) { NxVec3 nxCurrPos = getPosition(); // If the position is zero, // the parent hasn't been updated yet // and we don't even need to do the sweep test. // This is a fix for a problem that was happening // where on the add of the PxSingleActor, it would // set the position to a very small value because it would be getting a hit // even though the current position was 0. if ( nxCurrPos.isZero() ) return; // Set up the flags and the query structure. NxU32 flags = NX_SF_STATICS | NX_SF_DYNAMICS; NxSweepQueryHit sweepResult; dMemset( &sweepResult, 0, sizeof( sweepResult ) ); NxVec3 nxNewPos = mat->getPosition(); // Get the velocity which will be our sweep direction and distance. NxVec3 nxDir = nxNewPos - nxCurrPos; if ( nxDir.isZero() ) return; // Get the scene and do the sweep. mActor->wakeUp(); mActor->linearSweep( nxDir, flags, NULL, 1, &sweepResult, NULL ); if ( sweepResult.hitShape && sweepResult.t < nxDir.magnitude() ) { nxDir.normalize(); nxDir *= sweepResult.t; nxCurrPos += nxDir; mat->setPosition( Point3F( nxCurrPos.x, nxCurrPos.y, nxCurrPos.z ) ); } }
// Using swept code & direct position update (no physics engine) // This function is the generic character controller logic, valid for all swept volumes void SweepTest::MoveCharacter( void* user_data, void* user_data2, SweptVolume& volume, const NxVec3& direction, NxU32 nb_boxes, const NxExtendedBounds3* boxes, const void** box_user_data, NxU32 nb_capsules, const NxExtendedCapsule* capsules, const void** capsule_user_data, NxU32 groups, float min_dist, NxU32& collision_flags, const NxGroupsMask* groupsMask, bool constrainedClimbingMode ) { mHitNonWalkable = false; NxU32 CollisionFlags = 0; const NxU32 MaxIter = mMaxIter; // 1 for "collide and stop" const NxU32 MaxIterUp = MaxIter; const NxU32 MaxIterSides = MaxIter; // const NxU32 MaxIterDown = gWalkExperiment ? MaxIter : 1; const NxU32 MaxIterDown = 1; // ### this causes the artificial gap on top of chars float StepOffset = mStepOffset; // Default step offset can be cancelled in some cases. // Save initial height Extended OriginalHeight = volume.mCenter[mUpDirection]; Extended OriginalBottomPoint = OriginalHeight - volume.mHalfHeight; // UBI // TEST! Disable auto-step when flying. Not sure this is really useful. if(direction[mUpDirection]>0.0f) StepOffset = 0.0f; // Decompose motion into 3 independent motions: up, side, down // - if the motion is purely down (gravity only), the up part is needed to fight accuracy issues. For example if the // character is already touching the geometry a bit, the down sweep test might have troubles. If we first move it above // the geometry, the problems disappear. // - if the motion is lateral (character moving forward under normal gravity) the decomposition provides the autostep feature // - if the motion is purely up, the down part can be skipped NxVec3 UpVector(0.0f, 0.0f, 0.0f); NxVec3 DownVector(0.0f, 0.0f, 0.0f); if(direction[mUpDirection]<0.0f) DownVector[mUpDirection] = direction[mUpDirection]; else UpVector[mUpDirection] = direction[mUpDirection]; NxVec3 SideVector = direction; SideVector[mUpDirection] = 0.0f; // If the side motion is zero, i.e. if the character is not really moving, disable auto-step. if(!SideVector.isZero()) UpVector[mUpDirection] += StepOffset; // ==========[ Initial volume query ]=========================== if(1) { NxVec3 MotionExtents = UpVector; MotionExtents.max(SideVector); MotionExtents.max(DownVector); NxExtendedBounds3 TemporalBox; volume.ComputeTemporalBox(*this, TemporalBox, volume.mCenter, MotionExtents); // Gather touched geoms UpdateTouchedGeoms(user_data, volume, nb_boxes, boxes, box_user_data, nb_capsules, capsules, capsule_user_data, groups, TemporalBox, groupsMask); } // ==========[ UP PASS ]=========================== mCachedTriIndexIndex = 0; const bool PerformUpPass = true; NxU32 NbCollisions=0; if(PerformUpPass) { // Prevent user callback for up motion. This up displacement is artificial, and only needed for auto-stepping. // If we call the user for this, we might eventually apply upward forces to objects resting on top of us, even // if we visually don't move. This produces weird-looking motions. mValidateCallback = false; // In the walk-experiment we explicitely want to ban any up motions, to avoid characters climbing slopes they shouldn't climb. // So let's bypass the whole up pass. if(!mWalkExperiment) { // ### MaxIter here seems to "solve" the V bug if(DoSweepTest(user_data, user_data2, nb_boxes, boxes, box_user_data, nb_capsules, capsules, capsule_user_data, volume, UpVector, MaxIterUp, &NbCollisions, groups, min_dist, groupsMask)) { if(NbCollisions) { CollisionFlags |= NXCC_COLLISION_UP; // Clamp step offset to make sure we don't undo more than what we did Extended Delta = volume.mCenter[mUpDirection] - OriginalHeight; if(Delta<StepOffset) { StepOffset=float(Delta); } } } } } // ==========[ SIDE PASS ]=========================== mCachedTriIndexIndex = 1; mValidateCallback = true; const bool PerformSidePass = true; if(PerformSidePass) { NbCollisions=0; if(DoSweepTest(user_data, user_data2, nb_boxes, boxes, box_user_data, nb_capsules, capsules, capsule_user_data, volume, SideVector, MaxIterSides, &NbCollisions, groups, min_dist, groupsMask)) { if(NbCollisions) CollisionFlags |= NXCC_COLLISION_SIDES; } } // ==========[ DOWN PASS ]=========================== mCachedTriIndexIndex = 2; const bool PerformDownPass = true; if(PerformDownPass) { NbCollisions=0; if(!SideVector.isZero()) // We disabled that before so we don't have to undo it in that case DownVector[mUpDirection] -= StepOffset; // Undo our artificial up motion mValidTri = false; // min_dist actually makes a big difference :( // AAARRRGGH: if we get culled because of min_dist here, mValidTri never becomes valid! if(DoSweepTest(user_data, user_data2, nb_boxes, boxes, box_user_data, nb_capsules, capsules, capsule_user_data, volume, DownVector, MaxIterDown, &NbCollisions, groups, min_dist, groupsMask, true)) { if(NbCollisions) { CollisionFlags |= NXCC_COLLISION_DOWN; if(mHandleSlope) // PT: I think the following fix shouldn't be performed when mHandleSlope is false. { // PT: the following code is responsible for a weird capsule behaviour, // when colliding against a highly tesselated terrain: // - with a large direction vector, the capsule gets stuck against some part of the terrain // - with a slower direction vector (but in the same direction!) the capsule manages to move // I will keep that code nonetheless, since it seems to be useful for them. // constrainedClimbingMode if ( constrainedClimbingMode && mContactPointHeight > OriginalBottomPoint + StepOffset) { mHitNonWalkable = true; if(!mWalkExperiment) return; } //~constrainedClimbingMode } } } // TEST: do another down pass if we're on a non-walkable poly // ### kind of works but still not perfect // ### could it be because we zero the Y impulse later? // ### also check clamped response vectors if(mHandleSlope && mValidTri && direction[mUpDirection]<0.0f) { NxVec3 Normal; #ifdef USE_CONTACT_NORMAL_FOR_SLOPE_TEST Normal = mCN; #else mTouched.normal(Normal); #endif // if(fabsf(Normal|NxVec3(0.0f, 1.0f, 0.0f))<cosf(45.0f*DEGTORAD)) if(Normal[mUpDirection]>=0.0f && Normal[mUpDirection]<mSlopeLimit) { mHitNonWalkable = true; // Early exit if we're going to run this again anyway... if(!mWalkExperiment) return; /* CatchScene()->GetRenderer()->AddLine(mTouched.mVerts[0], mTouched.mVerts[1], ARGB_YELLOW); CatchScene()->GetRenderer()->AddLine(mTouched.mVerts[0], mTouched.mVerts[2], ARGB_YELLOW); CatchScene()->GetRenderer()->AddLine(mTouched.mVerts[1], mTouched.mVerts[2], ARGB_YELLOW); */ // ==========[ WALK EXPERIMENT ]=========================== mNormalizeResponse=true; Extended Delta = volume.mCenter[mUpDirection] > OriginalHeight ? volume.mCenter[mUpDirection] - OriginalHeight : 0.0f; Delta += fabsf(direction[mUpDirection]); Extended Recover = Delta; NbCollisions=0; const Extended MD = Recover < min_dist ? Recover/float(MaxIter) : min_dist; NxVec3 RecoverPoint(0,0,0); RecoverPoint[mUpDirection]=-float(Recover); if(DoSweepTest(user_data, user_data2, nb_boxes, boxes, box_user_data, nb_capsules, capsules, capsule_user_data, volume, RecoverPoint, MaxIter, &NbCollisions, groups, float(MD), groupsMask)) { // if(NbCollisions) CollisionFlags |= COLLISION_Y_DOWN; // PT: why did we do this ? Removed for now. It creates a bug (non registered event) when we land on a steep poly. // However this might have been needed when we were sliding on those polygons, and we didn't want the land anim to // start while we were sliding. // if(NbCollisions) CollisionFlags &= ~NXCC_COLLISION_DOWN; } mNormalizeResponse=false; } } } // Setup new collision flags collision_flags = CollisionFlags; }
// This is the generic sweep test for all swept volumes, but not character-controller specific bool SweepTest::DoSweepTest(void* user_data, void* user_data2, NxU32 nb_boxes, const NxExtendedBounds3* boxes, const void** box_user_data, NxU32 nb_capsules, const NxExtendedCapsule* capsules, const void** capsule_user_data, SweptVolume& swept_volume, const NxVec3& direction, NxU32 max_iter, NxU32* nb_collisions, NxU32 group_flags, float min_dist, const NxGroupsMask* groupsMask, bool down_pass) { // Early exit when motion is zero. Since the motion is decomposed into several vectors // and this function is called for each of them, it actually happens quite often. if(direction.isZero()) return false; bool HasMoved = false; mValidTri = false; NxExtendedVec3 CurrentPosition = swept_volume.mCenter; NxExtendedVec3 TargetPosition = swept_volume.mCenter; TargetPosition += direction; NxU32 NbCollisions = 0; while(max_iter--) { gNbIters++; // Compute current direction NxVec3 CurrentDirection = TargetPosition - CurrentPosition; // Make sure the new TBV is still valid { // Compute temporal bounding box. We could use a capsule or an OBB instead: // - the volume would be smaller // - but the query would be slower // Overall it's unclear whether it's worth it or not. // TODO: optimize this part ? NxExtendedBounds3 TemporalBox; swept_volume.ComputeTemporalBox(*this, TemporalBox, CurrentPosition, CurrentDirection); // Gather touched geoms UpdateTouchedGeoms(user_data, swept_volume, nb_boxes, boxes, box_user_data, nb_capsules, capsules, capsule_user_data, group_flags, TemporalBox, groupsMask); } const float Length = CurrentDirection.magnitude(); if(Length<min_dist) break; CurrentDirection /= Length; // From Quake2: "if velocity is against the original velocity, stop dead to avoid tiny occilations in sloping corners" if((CurrentDirection|direction) <= 0.0f) break; // From this point, we're going to update the position at least once HasMoved = true; // Find closest collision SweptContact C; C.mDistance = Length + mSkinWidth; if(!CollideGeoms(this, swept_volume, mGeomStream, CurrentPosition, CurrentDirection, C)) { // no collision found => move to desired position CurrentPosition = TargetPosition; break; } ASSERT(C.mGeom); // If we reach this point, we must have touched a geom if(C.mGeom->mType==TOUCHED_USER_BOX || C.mGeom->mType==TOUCHED_USER_CAPSULE) { // We touched a user object, typically another CCT if(mValidateCallback) UserHitCallback(user_data2, C, CurrentDirection, Length); // Trying to solve the following problem: // - by default, the CCT "friction" is infinite, i.e. a CCT will not slide on a slope (this is by design) // - this produces bad results when a capsule CCT stands on top of another capsule CCT, without sliding. Visually it looks // like the character is standing on the other character's head, it looks bad. So, here, we would like to let the CCT // slide away, i.e. we don't want friction. // So here we simply increase the number of iterations (== let the CCT slide) when the first down collision is with another CCT. if(down_pass && !NbCollisions) max_iter += 9; } else { // We touched a normal object #ifdef USE_CONTACT_NORMAL_FOR_SLOPE_TEST mValidTri = true; mCN = C.mWorldNormal; #else if(C.mIndex!=INVALID_ID) { mValidTri = true; mTouched = mWorldTriangles[C.mIndex]; } #endif { if(mValidateCallback) ShapeHitCallback(user_data2, C, CurrentDirection, Length); } } NbCollisions++; mContactPointHeight = (float)C.mWorldPos[mUpDirection]; // UBI const float DynSkin = mSkinWidth; if(C.mDistance>DynSkin/*+0.01f*/) CurrentPosition += CurrentDirection*(C.mDistance-DynSkin); NxVec3 WorldNormal = C.mWorldNormal; if(mWalkExperiment) { // Make sure the auto-step doesn't bypass this ! WorldNormal[mUpDirection]=0.0f; WorldNormal.normalize(); } const float Bump = 0.0f; // ### doesn't work when !=0 because of Quake2 hack! const float Friction = 1.0f; CollisionResponse(TargetPosition, CurrentPosition, CurrentDirection, WorldNormal, Bump, Friction, mNormalizeResponse); } if(nb_collisions) *nb_collisions = NbCollisions; // Final box position that should be reflected in the graphics engine swept_volume.mCenter = CurrentPosition; // If we didn't move, don't update the box position at all (keeping possible lazy-evaluated structures valid) return HasMoved; }