// // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of NVIDIA CORPORATION nor the names of its // contributors may be used to endorse or promote products derived // from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY // OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // Copyright (c) 2008-2019 NVIDIA Corporation. All rights reserved. // Copyright (c) 2004-2008 AGEIA Technologies, Inc. All rights reserved. // Copyright (c) 2001-2004 NovodeX AG. All rights reserved. #include "common/PxProfileZone.h" #include "geometry/PxMeshQuery.h" #include "PxRigidDynamic.h" #include "CctCharacterController.h" #include "CctCharacterControllerManager.h" #include "CctSweptBox.h" #include "CctSweptCapsule.h" #include "CctObstacleContext.h" #include "CmRenderOutput.h" #include "GuIntersectionBoxBox.h" #include "GuDistanceSegmentBox.h" #include "PsMathUtils.h" #include "PsFPU.h" // PT: TODO: remove those includes.... shouldn't be allowed from here #include "characterkinematic/PxControllerObstacles.h" // (*) #include "characterkinematic/PxControllerManager.h" // (*) #include "characterkinematic/PxControllerBehavior.h" // (*) #include "CctInternalStructs.h" // (*) //#define DEBUG_MTD #ifdef DEBUG_MTD #include #endif #define MAX_ITER 10 using namespace physx; using namespace Cct; using namespace Gu; using namespace Cm; static const PxU32 gObstacleDebugColor = PxU32(PxDebugColor::eARGB_CYAN); //static const PxU32 gCCTBoxDebugColor = PxU32(PxDebugColor::eARGB_YELLOW); static const PxU32 gTBVDebugColor = PxU32(PxDebugColor::eARGB_MAGENTA); static const bool gUsePartialUpdates = true; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static PX_FORCE_INLINE PxHitFlags getSweepHitFlags(const CCTParams& params) { PxHitFlags sweepHitFlags = PxHitFlag::eDEFAULT/*|PxHitFlag::eMESH_BOTH_SIDES*/; // sweepHitFlags |= PxHitFlag::eASSUME_NO_INITIAL_OVERLAP; if(params.mPreciseSweeps) sweepHitFlags |= PxHitFlag::ePRECISE_SWEEP; return sweepHitFlags; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static bool shouldApplyRecoveryModule(const PxRigidActor& rigidActor) { // PT: we must let the dynamic objects go through the CCT for proper 2-way interactions. // But we should still apply the recovery module for kinematics. const PxType type = rigidActor.getConcreteType(); if(type==PxConcreteType::eRIGID_STATIC) return true; if(type!=PxConcreteType::eRIGID_DYNAMIC) return false; return static_cast(rigidActor).getRigidBodyFlags() & PxRigidBodyFlag::eKINEMATIC; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static const bool gUseLocalSpace = true; static PxVec3 worldToLocal(const PxObstacle& obstacle, const PxExtendedVec3& worldPos) { const PxTransform tr(toVec3(obstacle.mPos), obstacle.mRot); return tr.transformInv(toVec3(worldPos)); } static PxVec3 localToWorld(const PxObstacle& obstacle, const PxVec3& localPos) { const PxTransform tr(toVec3(obstacle.mPos), obstacle.mRot); return tr.transform(localPos); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #ifdef PX_BIG_WORLDS typedef PxExtendedBounds3 PxCCTBounds3; typedef PxExtendedVec3 PxCCTVec3; #else typedef PxBounds3 PxCCTBounds3; typedef PxVec3 PxCCTVec3; #endif static PX_INLINE void scale(PxCCTBounds3& b, const PxVec3& scale) { PxCCTVec3 center; getCenter(b, center); PxVec3 extents; getExtents(b, extents); extents.x *= scale.x; extents.y *= scale.y; extents.z *= scale.z; setCenterExtents(b, center, extents); } static PX_INLINE void computeReflexionVector(PxVec3& reflected, const PxVec3& incomingDir, const PxVec3& outwardNormal) { reflected = incomingDir - outwardNormal * 2.0f * (incomingDir.dot(outwardNormal)); } static PX_INLINE void collisionResponse(PxExtendedVec3& targetPosition, const PxExtendedVec3& currentPosition, const PxVec3& currentDir, const PxVec3& hitNormal, PxF32 bump, PxF32 friction, bool normalize=false) { // Compute reflect direction PxVec3 reflectDir; computeReflexionVector(reflectDir, currentDir, hitNormal); reflectDir.normalize(); // Decompose it PxVec3 normalCompo, tangentCompo; Ps::decomposeVector(normalCompo, tangentCompo, reflectDir, hitNormal); // Compute new destination position const PxF32 amplitude = (targetPosition - currentPosition).magnitude(); targetPosition = currentPosition; if(bump!=0.0f) { if(normalize) normalCompo.normalize(); targetPosition += normalCompo*bump*amplitude; } if(friction!=0.0f) { if(normalize) tangentCompo.normalize(); targetPosition += tangentCompo*friction*amplitude; } } static PX_INLINE void relocateBox(PxBoxGeometry& boxGeom, PxTransform& pose, const PxExtendedVec3& center, const PxVec3& extents, const PxExtendedVec3& origin, const PxQuat& quatFromUp) { boxGeom.halfExtents = extents; pose.p.x = float(center.x - origin.x); pose.p.y = float(center.y - origin.y); pose.p.z = float(center.z - origin.z); pose.q = quatFromUp; } static PX_INLINE void relocateBox(PxBoxGeometry& boxGeom, PxTransform& pose, const TouchedUserBox& userBox) { relocateBox(boxGeom, pose, userBox.mBox.center, userBox.mBox.extents, userBox.mOffset, userBox.mBox.rot); } static PX_INLINE void relocateBox(PxBoxGeometry& boxGeom, PxTransform& pose, const TouchedBox& box) { boxGeom.halfExtents = box.mExtents; pose.p = box.mCenter; pose.q = box.mRot; } static PX_INLINE void relocateCapsule( PxCapsuleGeometry& capsuleGeom, PxTransform& pose, const SweptCapsule* sc, const PxQuat& quatFromUp, const PxExtendedVec3& center, const PxExtendedVec3& origin) { capsuleGeom.radius = sc->mRadius; capsuleGeom.halfHeight = 0.5f * sc->mHeight; pose.p.x = float(center.x - origin.x); pose.p.y = float(center.y - origin.y); pose.p.z = float(center.z - origin.z); pose.q = quatFromUp; } static PX_INLINE void relocateCapsule(PxCapsuleGeometry& capsuleGeom, PxTransform& pose, const PxVec3& p0, const PxVec3& p1, PxReal radius) { capsuleGeom.radius = radius; pose = PxTransformFromSegment(p0, p1, &capsuleGeom.halfHeight); if(capsuleGeom.halfHeight==0.0f) capsuleGeom.halfHeight = FLT_EPSILON; } static PX_INLINE void relocateCapsule(PxCapsuleGeometry& capsuleGeom, PxTransform& pose, const TouchedUserCapsule& userCapsule) { PxVec3 p0, p1; p0.x = float(userCapsule.mCapsule.p0.x - userCapsule.mOffset.x); p0.y = float(userCapsule.mCapsule.p0.y - userCapsule.mOffset.y); p0.z = float(userCapsule.mCapsule.p0.z - userCapsule.mOffset.z); p1.x = float(userCapsule.mCapsule.p1.x - userCapsule.mOffset.x); p1.y = float(userCapsule.mCapsule.p1.y - userCapsule.mOffset.y); p1.z = float(userCapsule.mCapsule.p1.z - userCapsule.mOffset.z); relocateCapsule(capsuleGeom, pose, p0, p1, userCapsule.mCapsule.radius); } static bool SweepBoxUserBox(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eBOX); PX_ASSERT(geom->mType==TouchedGeomType::eUSER_BOX); const SweptBox* SB = static_cast(volume); const TouchedUserBox* TC = static_cast(geom); PxBoxGeometry boxGeom0; PxTransform boxPose0; // To precompute relocateBox(boxGeom0, boxPose0, center, SB->mExtents, TC->mOffset, test->mUserParams.mQuatFromUp); PxBoxGeometry boxGeom1; PxTransform boxPose1; relocateBox(boxGeom1, boxPose1, *TC); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, boxGeom0, boxPose0, boxGeom1, boxPose1, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mWorldNormal = sweepHit.normal; impact.mDistance = sweepHit.distance; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; impact.setWorldPos(sweepHit.position, TC->mOffset); return true; } static bool SweepBoxUserCapsule(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eBOX); PX_ASSERT(geom->mType==TouchedGeomType::eUSER_CAPSULE); const SweptBox* SB = static_cast(volume); const TouchedUserCapsule* TC = static_cast(geom); PxBoxGeometry boxGeom; PxTransform boxPose; // To precompute relocateBox(boxGeom, boxPose, center, SB->mExtents, TC->mOffset, test->mUserParams.mQuatFromUp); PxCapsuleGeometry capsuleGeom; PxTransform capsulePose; relocateCapsule(capsuleGeom, capsulePose, *TC); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, boxGeom, boxPose, capsuleGeom, capsulePose, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; //TO CHECK: Investigate whether any significant performance improvement can be achieved through // making the impact point computation optional in the sweep calls and compute it later /*{ // ### check this float t; PxVec3 p; float d = gUtilLib->PxSegmentOBBSqrDist(Capsule, Box0.center, Box0.extents, Box0.rot, &t, &p); Box0.rot.multiply(p,p); impact.mWorldPos.x = p.x + Box0.center.x + TC->mOffset.x; impact.mWorldPos.y = p.y + Box0.center.y + TC->mOffset.y; impact.mWorldPos.z = p.z + Box0.center.z + TC->mOffset.z; }*/ { impact.setWorldPos(sweepHit.position, TC->mOffset); } return true; } static bool sweepVolumeVsMesh( const SweepTest* sweepTest, const TouchedMesh* touchedMesh, SweptContact& impact, const PxVec3& unitDir, const PxGeometry& geom, const PxTransform& pose, PxU32 nbTris, const PxTriangle* triangles, PxU32 cachedIndex) { PxSweepHit sweepHit; if(PxMeshQuery::sweep(unitDir, impact.mDistance, geom, pose, nbTris, triangles, sweepHit, getSweepHitFlags(sweepTest->mUserParams), &cachedIndex)) { if(sweepHit.distance >= impact.mDistance) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.setWorldPos(sweepHit.position, touchedMesh->mOffset); // Returned index is only between 0 and nbTris, i.e. it indexes the array of cached triangles, not the original mesh. PX_ASSERT(sweepHit.faceIndex < nbTris); sweepTest->mCachedTriIndex[sweepTest->mCachedTriIndexIndex] = sweepHit.faceIndex; // The CCT loop will use the index from the start of the cache... impact.mInternalIndex = sweepHit.faceIndex + touchedMesh->mIndexWorldTriangles; const PxU32* triangleIndices = &sweepTest->mTriangleIndices[touchedMesh->mIndexWorldTriangles]; impact.mTriangleIndex = triangleIndices[sweepHit.faceIndex]; return true; } return false; } static bool SweepBoxMesh(const SweepTest* sweep_test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eBOX); PX_ASSERT(geom->mType==TouchedGeomType::eMESH); const SweptBox* SB = static_cast(volume); const TouchedMesh* TM = static_cast(geom); PxU32 nbTris = TM->mNbTris; if(!nbTris) return false; // Fetch triangle data for current mesh (the stream may contain triangles from multiple meshes) const PxTriangle* T = &sweep_test->mWorldTriangles.getTriangle(TM->mIndexWorldTriangles); // PT: this only really works when the CCT collides with a single mesh, but that's the most common case. When it doesn't, there's just no speedup but it still works. PxU32 CachedIndex = sweep_test->mCachedTriIndex[sweep_test->mCachedTriIndexIndex]; if(CachedIndex>=nbTris) CachedIndex=0; PxBoxGeometry boxGeom; boxGeom.halfExtents = SB->mExtents; PxTransform boxPose(PxVec3(float(center.x - TM->mOffset.x), float(center.y - TM->mOffset.y), float(center.z - TM->mOffset.z)), sweep_test->mUserParams.mQuatFromUp); // Precompute return sweepVolumeVsMesh(sweep_test, TM, impact, dir, boxGeom, boxPose, nbTris, T, CachedIndex); } static bool SweepCapsuleMesh( const SweepTest* sweep_test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eCAPSULE); PX_ASSERT(geom->mType==TouchedGeomType::eMESH); const SweptCapsule* SC = static_cast(volume); const TouchedMesh* TM = static_cast(geom); PxU32 nbTris = TM->mNbTris; if(!nbTris) return false; // Fetch triangle data for current mesh (the stream may contain triangles from multiple meshes) const PxTriangle* T = &sweep_test->mWorldTriangles.getTriangle(TM->mIndexWorldTriangles); // PT: this only really works when the CCT collides with a single mesh, but that's the most common case. // When it doesn't, there's just no speedup but it still works. PxU32 CachedIndex = sweep_test->mCachedTriIndex[sweep_test->mCachedTriIndexIndex]; if(CachedIndex>=nbTris) CachedIndex=0; PxCapsuleGeometry capsuleGeom; PxTransform capsulePose; relocateCapsule(capsuleGeom, capsulePose, SC, sweep_test->mUserParams.mQuatFromUp, center, TM->mOffset); return sweepVolumeVsMesh(sweep_test, TM, impact, dir, capsuleGeom, capsulePose, nbTris, T, CachedIndex); } static bool SweepBoxBox(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eBOX); PX_ASSERT(geom->mType==TouchedGeomType::eBOX); const SweptBox* SB = static_cast(volume); const TouchedBox* TB = static_cast(geom); PxBoxGeometry boxGeom0; PxTransform boxPose0; // To precompute relocateBox(boxGeom0, boxPose0, center, SB->mExtents, TB->mOffset, test->mUserParams.mQuatFromUp); PxBoxGeometry boxGeom1; PxTransform boxPose1; relocateBox(boxGeom1, boxPose1, *TB); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, boxGeom0, boxPose0, boxGeom1, boxPose1, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mWorldNormal = sweepHit.normal; impact.mDistance = sweepHit.distance; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; impact.setWorldPos(sweepHit.position, TB->mOffset); return true; } static bool SweepBoxSphere(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eBOX); PX_ASSERT(geom->mType==TouchedGeomType::eSPHERE); const SweptBox* SB = static_cast(volume); const TouchedSphere* TS = static_cast(geom); PxBoxGeometry boxGeom; PxTransform boxPose; // To precompute relocateBox(boxGeom, boxPose, center, SB->mExtents, TS->mOffset, test->mUserParams.mQuatFromUp); PxSphereGeometry sphereGeom; sphereGeom.radius = TS->mRadius; PxTransform spherePose; spherePose.p = TS->mCenter; spherePose.q = PxQuat(PxIdentity); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, boxGeom, boxPose, sphereGeom, spherePose, sweepHit, getSweepHitFlags(test->mUserParams))) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; //TO CHECK: Investigate whether any significant performance improvement can be achieved through // making the impact point computation optional in the sweep calls and compute it later /* { // The sweep test doesn't compute the impact point automatically, so we have to do it here. PxVec3 NewSphereCenter = TS->mSphere.center - d * dir; PxVec3 Closest; gUtilLib->PxPointOBBSqrDist(NewSphereCenter, Box0.center, Box0.extents, Box0.rot, &Closest); // Compute point on the box, after sweep Box0.rot.multiply(Closest, Closest); impact.mWorldPos.x = TS->mOffset.x + Closest.x + Box0.center.x + d * dir.x; impact.mWorldPos.y = TS->mOffset.y + Closest.y + Box0.center.y + d * dir.y; impact.mWorldPos.z = TS->mOffset.z + Closest.z + Box0.center.z + d * dir.z; impact.mWorldNormal = -impact.mWorldNormal; }*/ { impact.setWorldPos(sweepHit.position, TS->mOffset); } return true; } static bool SweepBoxCapsule(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eBOX); PX_ASSERT(geom->mType==TouchedGeomType::eCAPSULE); const SweptBox* SB = static_cast(volume); const TouchedCapsule* TC = static_cast(geom); PxBoxGeometry boxGeom; PxTransform boxPose; // To precompute relocateBox(boxGeom, boxPose, center, SB->mExtents, TC->mOffset, test->mUserParams.mQuatFromUp); PxCapsuleGeometry capsuleGeom; PxTransform capsulePose; relocateCapsule(capsuleGeom, capsulePose, TC->mP0, TC->mP1, TC->mRadius); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, boxGeom, boxPose, capsuleGeom, capsulePose, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; //TO CHECK: Investigate whether any significant performance improvement can be achieved through // making the impact point computation optional in the sweep calls and compute it later /*{ float t; PxVec3 p; float d = gUtilLib->PxSegmentOBBSqrDist(TC->mCapsule, Box0.center, Box0.extents, Box0.rot, &t, &p); Box0.rot.multiply(p,p); impact.mWorldPos.x = p.x + Box0.center.x + TC->mOffset.x; impact.mWorldPos.y = p.y + Box0.center.y + TC->mOffset.y; impact.mWorldPos.z = p.z + Box0.center.z + TC->mOffset.z; }*/ { impact.setWorldPos(sweepHit.position, TC->mOffset); } return true; } static bool SweepCapsuleBox(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eCAPSULE); PX_ASSERT(geom->mType==TouchedGeomType::eBOX); const SweptCapsule* SC = static_cast(volume); const TouchedBox* TB = static_cast(geom); PxCapsuleGeometry capsuleGeom; PxTransform capsulePose; relocateCapsule(capsuleGeom, capsulePose, SC, test->mUserParams.mQuatFromUp, center, TB->mOffset); PxBoxGeometry boxGeom; PxTransform boxPose; // To precompute relocateBox(boxGeom, boxPose, *TB); // The box and capsule coordinates are relative to the center of the cached bounding box PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, capsuleGeom, capsulePose, boxGeom, boxPose, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; //TO CHECK: Investigate whether any significant performance improvement can be achieved through // making the impact point computation optional in the sweep calls and compute it later /*{ float t; PxVec3 p; float d = gUtilLib->PxSegmentOBBSqrDist(Capsule, TB->mBox.center, TB->mBox.extents, TB->mBox.rot, &t, &p); TB->mBox.rot.multiply(p,p); p += TB->mBox.center; impact.mWorldPos.x = p.x + TB->mOffset.x; impact.mWorldPos.y = p.y + TB->mOffset.y; impact.mWorldPos.z = p.z + TB->mOffset.z; }*/ { impact.setWorldPos(sweepHit.position, TB->mOffset); } return true; } static bool SweepCapsuleSphere(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eCAPSULE); PX_ASSERT(geom->mType==TouchedGeomType::eSPHERE); const SweptCapsule* SC = static_cast(volume); const TouchedSphere* TS = static_cast(geom); PxCapsuleGeometry capsuleGeom; PxTransform capsulePose; relocateCapsule(capsuleGeom, capsulePose, SC, test->mUserParams.mQuatFromUp, center, TS->mOffset); PxSphereGeometry sphereGeom; sphereGeom.radius = TS->mRadius; PxTransform spherePose; spherePose.p = TS->mCenter; spherePose.q = PxQuat(PxIdentity); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, capsuleGeom, capsulePose, sphereGeom, spherePose, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; impact.setWorldPos(sweepHit.position, TS->mOffset); return true; } static bool SweepCapsuleCapsule(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eCAPSULE); PX_ASSERT(geom->mType==TouchedGeomType::eCAPSULE); const SweptCapsule* SC = static_cast(volume); const TouchedCapsule* TC = static_cast(geom); PxCapsuleGeometry capsuleGeom0; PxTransform capsulePose0; relocateCapsule(capsuleGeom0, capsulePose0, SC, test->mUserParams.mQuatFromUp, center, TC->mOffset); PxCapsuleGeometry capsuleGeom1; PxTransform capsulePose1; relocateCapsule(capsuleGeom1, capsulePose1, TC->mP0, TC->mP1, TC->mRadius); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, capsuleGeom0, capsulePose0, capsuleGeom1, capsulePose1, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; impact.setWorldPos(sweepHit.position, TC->mOffset); return true; } static bool SweepCapsuleUserCapsule(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eCAPSULE); PX_ASSERT(geom->mType==TouchedGeomType::eUSER_CAPSULE); const SweptCapsule* SC = static_cast(volume); const TouchedUserCapsule* TC = static_cast(geom); PxCapsuleGeometry capsuleGeom0; PxTransform capsulePose0; relocateCapsule(capsuleGeom0, capsulePose0, SC, test->mUserParams.mQuatFromUp, center, TC->mOffset); PxCapsuleGeometry capsuleGeom1; PxTransform capsulePose1; relocateCapsule(capsuleGeom1, capsulePose1, *TC); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, capsuleGeom0, capsulePose0, capsuleGeom1, capsulePose1, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; impact.setWorldPos(sweepHit.position, TC->mOffset); return true; } static bool SweepCapsuleUserBox(const SweepTest* test, const SweptVolume* volume, const TouchedGeom* geom, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact) { PX_ASSERT(volume->getType()==SweptVolumeType::eCAPSULE); PX_ASSERT(geom->mType==TouchedGeomType::eUSER_BOX); const SweptCapsule* SC = static_cast(volume); const TouchedUserBox* TB = static_cast(geom); PxCapsuleGeometry capsuleGeom; PxTransform capsulePose; relocateCapsule(capsuleGeom, capsulePose, SC, test->mUserParams.mQuatFromUp, center, TB->mOffset); PxBoxGeometry boxGeom; PxTransform boxPose; relocateBox(boxGeom, boxPose, *TB); PxSweepHit sweepHit; if(!PxGeometryQuery::sweep(dir, impact.mDistance, capsuleGeom, capsulePose, boxGeom, boxPose, sweepHit, getSweepHitFlags(test->mUserParams))) return false; if(sweepHit.distance >= impact.mDistance) return false; impact.mDistance = sweepHit.distance; impact.mWorldNormal = sweepHit.normal; impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; //TO CHECK: Investigate whether any significant performance improvement can be achieved through // making the impact point computation optional in the sweep calls and compute it later /*{ // ### check this float t; PxVec3 p; float d = gUtilLib->PxSegmentOBBSqrDist(Capsule, Box.center, Box.extents, Box.rot, &t, &p); p += Box.center; impact.mWorldPos.x = p.x + TB->mOffset.x; impact.mWorldPos.y = p.y + TB->mOffset.y; impact.mWorldPos.z = p.z + TB->mOffset.z; }*/ { impact.setWorldPos(sweepHit.position, TB->mOffset); } return true; } typedef bool (*SweepFunc) (const SweepTest*, const SweptVolume*, const TouchedGeom*, const PxExtendedVec3&, const PxVec3&, SweptContact&); static SweepFunc gSweepMap[SweptVolumeType::eLAST][TouchedGeomType::eLAST] = { // Box funcs { SweepBoxUserBox, SweepBoxUserCapsule, SweepBoxMesh, SweepBoxBox, SweepBoxSphere, SweepBoxCapsule }, // Capsule funcs { SweepCapsuleUserBox, SweepCapsuleUserCapsule, SweepCapsuleMesh, SweepCapsuleBox, SweepCapsuleSphere, SweepCapsuleCapsule } }; PX_COMPILE_TIME_ASSERT(sizeof(gSweepMap)==SweptVolumeType::eLAST*TouchedGeomType::eLAST*sizeof(SweepFunc)); static const PxU32 GeomSizes[] = { sizeof(TouchedUserBox), sizeof(TouchedUserCapsule), sizeof(TouchedMesh), sizeof(TouchedBox), sizeof(TouchedSphere), sizeof(TouchedCapsule), }; static const TouchedGeom* CollideGeoms( const SweepTest* sweep_test, const SweptVolume& volume, const IntArray& geom_stream, const PxExtendedVec3& center, const PxVec3& dir, SweptContact& impact, bool discardInitialOverlap) { impact.mInternalIndex = PX_INVALID_U32; impact.mTriangleIndex = PX_INVALID_U32; impact.mGeom = NULL; const PxU32* Data = geom_stream.begin(); const PxU32* Last = geom_stream.end(); while(Data!=Last) { const TouchedGeom* CurrentGeom = reinterpret_cast(Data); SweepFunc ST = gSweepMap[volume.getType()][CurrentGeom->mType]; if(ST) { SweptContact C; C.mDistance = impact.mDistance; // Initialize with current best distance C.mInternalIndex = PX_INVALID_U32; C.mTriangleIndex = PX_INVALID_U32; if((ST)(sweep_test, &volume, CurrentGeom, center, dir, C)) { if(C.mDistance==0.0f) { if(!discardInitialOverlap) { if(CurrentGeom->mType==TouchedGeomType::eUSER_BOX || CurrentGeom->mType==TouchedGeomType::eUSER_CAPSULE) { } else { const PxRigidActor* touchedActor = CurrentGeom->mActor; PX_ASSERT(touchedActor); if(shouldApplyRecoveryModule(*touchedActor)) { impact = C; impact.mGeom = const_cast(CurrentGeom); return CurrentGeom; } } } } /* else if(discardInitialOverlap && C.mDistance==0.0f) { // PT: we previously used eINITIAL_OVERLAP without eINITIAL_OVERLAP_KEEP, i.e. initially overlapping shapes got ignored. // So we replicate this behavior here. }*/ else if(C.mDistance(CurrentGeom); if(C.mDistance <= 0.0f) // there is no point testing for closer hits return CurrentGeom; // since we are touching a shape already } } } const PxU8* ptr = reinterpret_cast(Data); ptr += GeomSizes[CurrentGeom->mType]; Data = reinterpret_cast(ptr); } return impact.mGeom; } static PxVec3 computeMTD(const SweepTest* sweep_test, const SweptVolume& volume, const IntArray& geom_stream, const PxExtendedVec3& center, float contactOffset) { PxVec3 p = toVec3(center); // contactOffset += 0.01f; const PxU32 maxIter = 4; PxU32 nbIter = 0; bool isValid = true; while(isValid && nbIter(Data); if(CurrentGeom->mType==TouchedGeomType::eUSER_BOX || CurrentGeom->mType==TouchedGeomType::eUSER_CAPSULE) { } else { const PxRigidActor* touchedActor = CurrentGeom->mActor; PX_ASSERT(touchedActor); if(shouldApplyRecoveryModule(*touchedActor)) { const PxShape* touchedShape = reinterpret_cast(CurrentGeom->mTGUserData); PX_ASSERT(touchedShape); const PxGeometryHolder gh = touchedShape->getGeometry(); const PxTransform globalPose = getShapeGlobalPose(*touchedShape, *touchedActor); PxVec3 mtd; PxF32 depth; const PxTransform volumePose(p, sweep_test->mUserParams.mQuatFromUp); if(volume.getType()==SweptVolumeType::eCAPSULE) { const SweptCapsule& sc = static_cast(volume); const PxCapsuleGeometry capsuleGeom(sc.mRadius+contactOffset, sc.mHeight*0.5f); isValid = PxGeometryQuery::computePenetration(mtd, depth, capsuleGeom, volumePose, gh.any(), globalPose); } else { PX_ASSERT(volume.getType()==SweptVolumeType::eBOX); const SweptBox& sb = static_cast(volume); const PxBoxGeometry boxGeom(sb.mExtents+PxVec3(contactOffset)); isValid = PxGeometryQuery::computePenetration(mtd, depth, boxGeom, volumePose, gh.any(), globalPose); } if(isValid) { nbIter++; PX_ASSERT(depth>=0.0f); PX_ASSERT(mtd.isFinite()); PX_ASSERT(PxIsFinite(depth)); #ifdef DEBUG_MTD PX_ASSERT(depth<=1.0f); if(depth>1.0f || !mtd.isFinite() || !PxIsFinite(depth)) { int stop=1; (void)stop; } printf("Depth: %f\n", depth); printf("mtd: %f %f %f\n", mtd.x, mtd.y, mtd.z); #endif p += mtd * depth; } } } const PxU8* ptr = reinterpret_cast(Data); ptr += GeomSizes[CurrentGeom->mType]; Data = reinterpret_cast(ptr); } } return p; } static bool ParseGeomStream(const void* object, const IntArray& geom_stream) { const PxU32* Data = geom_stream.begin(); const PxU32* Last = geom_stream.end(); while(Data!=Last) { const TouchedGeom* CurrentGeom = reinterpret_cast(Data); if(CurrentGeom->mTGUserData==object) return true; const PxU8* ptr = reinterpret_cast(Data); ptr += GeomSizes[CurrentGeom->mType]; Data = reinterpret_cast(ptr); } return false; } CCTParams::CCTParams() : mNonWalkableMode (PxControllerNonWalkableMode::ePREVENT_CLIMBING), mQuatFromUp (PxQuat(PxIdentity)), mUpDirection (PxVec3(0.0f)), mSlopeLimit (0.0f), mContactOffset (0.0f), mStepOffset (0.0f), mInvisibleWallHeight (0.0f), mMaxJumpHeight (0.0f), mMaxEdgeLength2 (0.0f), mTessellation (false), mHandleSlope (false), mOverlapRecovery (false), mPreciseSweeps (true), mPreventVerticalSlidingAgainstCeiling (false) { } SweepTest::SweepTest(bool registerDeletionListener) : mRenderBuffer (NULL), mRenderFlags (0), mTriangleIndices (PX_DEBUG_EXP("sweepTestTriangleIndices")), mGeomStream (PX_DEBUG_EXP("sweepTestStream")), mTouchedShape (registerDeletionListener), mTouchedActor (registerDeletionListener), mSQTimeStamp (0xffffffff), mNbFullUpdates (0), mNbPartialUpdates (0), mNbTessellation (0), mNbIterations (0), mFlags (0), mRegisterDeletionListener(registerDeletionListener), mCctManager (NULL) { mCacheBounds.setEmpty(); mCachedTriIndexIndex = 0; mCachedTriIndex[0] = mCachedTriIndex[1] = mCachedTriIndex[2] = 0; mNbCachedStatic = 0; mNbCachedT = 0; mTouchedObstacleHandle = INVALID_OBSTACLE_HANDLE; mTouchedPos = PxVec3(0); mTouchedPosShape_Local = PxVec3(0); mTouchedPosShape_World = PxVec3(0); mTouchedPosObstacle_Local = PxVec3(0); mTouchedPosObstacle_World = PxVec3(0); // mVolumeGrowth = 1.2f; // Must be >1.0f and not too big mVolumeGrowth = 1.5f; // Must be >1.0f and not too big // mVolumeGrowth = 2.0f; // Must be >1.0f and not too big mContactNormalDownPass = PxVec3(0.0f); mContactNormalSidePass = PxVec3(0.0f); mTouchedTriMin = 0.0f; mTouchedTriMax = 0.0f; } SweepTest::~SweepTest() { // set the TouchedObject to NULL so we unregister the actor/shape mTouchedShape = NULL; mTouchedActor = NULL; } void SweepTest::voidTestCache() { mTouchedShape = NULL; mTouchedActor = NULL; mCacheBounds.setEmpty(); mTouchedObstacleHandle = INVALID_OBSTACLE_HANDLE; } void SweepTest::onRelease(const PxBase& observed) { if (mTouchedActor == &observed) { mTouchedShape = NULL; mTouchedActor = NULL; return; } if(ParseGeomStream(&observed, mGeomStream)) mCacheBounds.setEmpty(); if (mTouchedShape == &observed) mTouchedShape = NULL; } void SweepTest::updateCachedShapesRegistration(PxU32 startIndex, bool unregister) { if(!mRegisterDeletionListener) return; if(!mGeomStream.size() || startIndex == mGeomStream.size()) return; PX_ASSERT(startIndex <= mGeomStream.size()); const PxU32* data = &mGeomStream[startIndex]; const PxU32* last = mGeomStream.end(); while (data != last) { const TouchedGeom* CurrentGeom = reinterpret_cast(data); if (CurrentGeom->mActor) { if(unregister) mCctManager->unregisterObservedObject(reinterpret_cast(CurrentGeom->mTGUserData)); else mCctManager->registerObservedObject(reinterpret_cast(CurrentGeom->mTGUserData)); } else { // we can early exit, the rest of the data are user obstacles return; } const PxU8* ptr = reinterpret_cast(data); ptr += GeomSizes[CurrentGeom->mType]; data = reinterpret_cast(ptr); } } void SweepTest::onObstacleAdded(ObstacleHandle index, const PxObstacleContext* context, const PxVec3& origin, const PxVec3& unitDir, const PxReal distance ) { if(mTouchedObstacleHandle != INVALID_OBSTACLE_HANDLE) { // check if new obstacle is closer const ObstacleContext* obstContext = static_cast (context); PxRaycastHit obstacleHit; const PxObstacle* obst = obstContext->raycastSingle(obstacleHit,index,origin,unitDir,distance); if(obst && (obstacleHit.position.dot(unitDir))<(mTouchedPosObstacle_World.dot(unitDir))) { PX_ASSERT(obstacleHit.distance<=distance); mTouchedObstacleHandle = index; if(!gUseLocalSpace) { mTouchedPos = toVec3(obst->mPos); } else { mTouchedPosObstacle_World = obstacleHit.position; mTouchedPosObstacle_Local = worldToLocal(*obst, PxExtendedVec3(PxExtended(obstacleHit.position.x),PxExtended(obstacleHit.position.y),PxExtended(obstacleHit.position.z))); } } } } void SweepTest::onObstacleRemoved(ObstacleHandle index) { if(index == mTouchedObstacleHandle) { mTouchedObstacleHandle = INVALID_OBSTACLE_HANDLE; } } void SweepTest::onObstacleUpdated(ObstacleHandle index, const PxObstacleContext* context, const PxVec3& origin, const PxVec3& unitDir, const PxReal distance) { if(index == mTouchedObstacleHandle) { // check if updated obstacle is still closest const ObstacleContext* obstContext = static_cast (context); PxRaycastHit obstacleHit; ObstacleHandle closestHandle = INVALID_OBSTACLE_HANDLE; const PxObstacle* obst = obstContext->raycastSingle(obstacleHit,origin,unitDir,distance,closestHandle); if(mTouchedObstacleHandle == closestHandle) return; if(obst) { PX_ASSERT(obstacleHit.distance<=distance); mTouchedObstacleHandle = closestHandle; if(!gUseLocalSpace) { mTouchedPos = toVec3(obst->mPos); } else { mTouchedPosObstacle_World = obstacleHit.position; mTouchedPosObstacle_Local = worldToLocal(*obst, PxExtendedVec3(PxExtended(obstacleHit.position.x),PxExtended(obstacleHit.position.y),PxExtended(obstacleHit.position.z))); } } } } void SweepTest::onOriginShift(const PxVec3& shift) { mCacheBounds.minimum -= shift; mCacheBounds.maximum -= shift; if(mTouchedShape) { const PxRigidActor* rigidActor = mTouchedActor.get(); if(rigidActor->getConcreteType() != PxConcreteType::eRIGID_STATIC) { mTouchedPosShape_World -= shift; } } else if (mTouchedObstacleHandle != INVALID_OBSTACLE_HANDLE) { if(!gUseLocalSpace) { mTouchedPos -= shift; } else { mTouchedPosObstacle_World -= shift; } } // adjust cache PxU32* data = mGeomStream.begin(); PxU32* last = mGeomStream.end(); while(data != last) { TouchedGeom* currentGeom = reinterpret_cast(data); currentGeom->mOffset -= shift; PxU8* ptr = reinterpret_cast(data); ptr += GeomSizes[currentGeom->mType]; data = reinterpret_cast(ptr); } } static PxBounds3 getBounds3(const PxExtendedBounds3& extended) { return PxBounds3(toVec3(extended.minimum), toVec3(extended.maximum)); // LOSS OF ACCURACY } // PT: finds both touched CCTs and touched user-defined obstacles void SweepTest::findTouchedObstacles(const UserObstacles& userObstacles, const PxExtendedBounds3& worldBox) { PxExtendedVec3 Origin; // Will be TouchedGeom::mOffset getCenter(worldBox, Origin); { const PxU32 nbBoxes = userObstacles.mNbBoxes; const PxExtendedBox* boxes = userObstacles.mBoxes; const void** boxUserData = userObstacles.mBoxUserData; const PxBounds3 singlePrecisionWorldBox = getBounds3(worldBox); // Find touched boxes, i.e. other box controllers for(PxU32 i=0;i(reserveContainerMemory(mGeomStream, sizeof(TouchedUserBox)/sizeof(PxU32))); UserBox->mType = TouchedGeomType::eUSER_BOX; UserBox->mTGUserData = boxUserData[i]; UserBox->mActor = NULL; UserBox->mOffset = Origin; UserBox->mBox = boxes[i]; } } { // Find touched capsules, i.e. other capsule controllers const PxU32 nbCapsules = userObstacles.mNbCapsules; const PxExtendedCapsule* capsules = userObstacles.mCapsules; const void** capsuleUserData = userObstacles.mCapsuleUserData; PxExtendedVec3 Center; PxVec3 Extents; getCenter(worldBox, Center); getExtents(worldBox, Extents); for(PxU32 i=0;i worldBox.maximum.x) || (worldBox.minimum.x > capMaxx + PxExtended(r))) continue; const PxExtended capMiny = PxMin(capsules[i].p0.y, capsules[i].p1.y); const PxExtended capMaxy = PxMax(capsules[i].p0.y, capsules[i].p1.y); if((capMiny - PxExtended(r) > worldBox.maximum.y) || (worldBox.minimum.y > capMaxy + PxExtended(r))) continue; const PxExtended capMinz = PxMin(capsules[i].p0.z, capsules[i].p1.z); const PxExtended capMaxz = PxMax(capsules[i].p0.z, capsules[i].p1.z); if((capMinz - PxExtended(r) > worldBox.maximum.z) || (worldBox.minimum.z > capMaxz + PxExtended(r))) continue; // PT: more accurate capsule-box test. Not strictly necessary but worth doing if available const PxReal d2 = Gu::distanceSegmentBoxSquared(toVec3(capsules[i].p0), toVec3(capsules[i].p1), toVec3(Center), Extents, PxMat33(PxIdentity)); if(d2>r*r) continue; TouchedUserCapsule* UserCapsule = reinterpret_cast(reserveContainerMemory(mGeomStream, sizeof(TouchedUserCapsule)/sizeof(PxU32))); UserCapsule->mType = TouchedGeomType::eUSER_CAPSULE; UserCapsule->mTGUserData = capsuleUserData[i]; UserCapsule->mActor = NULL; UserCapsule->mOffset = Origin; UserCapsule->mCapsule = capsules[i]; } } } void SweepTest::updateTouchedGeoms( const InternalCBData_FindTouchedGeom* userData, const UserObstacles& userObstacles, const PxExtendedBounds3& worldTemporalBox, const PxControllerFilters& filters, const PxVec3& sideVector) { /* - if this is the first iteration (new frame) we have to redo the dynamic objects & the CCTs. The static objects can be cached. - if this is not, we can cache everything */ // PT: using "worldTemporalBox" instead of "mCacheBounds" seems to produce TTP 6207 //#define DYNAMIC_BOX worldTemporalBox #define DYNAMIC_BOX mCacheBounds bool newCachedBox = false; CCTFilter filter; filter.mFilterData = filters.mFilterData; filter.mFilterCallback = filters.mFilterCallback; filter.mPreFilter = filters.mFilterFlags & PxQueryFlag::ePREFILTER; filter.mPostFilter = filters.mFilterFlags & PxQueryFlag::ePOSTFILTER; // PT: detect changes to the static pruning structure bool sceneHasChanged = false; { const PxU32 currentTimestamp = getSceneTimestamp(userData); if(currentTimestamp!=mSQTimeStamp) { mSQTimeStamp = currentTimestamp; sceneHasChanged = true; } } // If the input box is inside the cached box, nothing to do if(gUsePartialUpdates && !sceneHasChanged && worldTemporalBox.isInside(mCacheBounds)) { //printf("CACHEIN%d\n", mFirstUpdate); if(mFlags & STF_FIRST_UPDATE) { mFlags &= ~STF_FIRST_UPDATE; // Only redo the dynamic updateCachedShapesRegistration(mNbCachedStatic, true); mGeomStream.forceSize_Unsafe(mNbCachedStatic); mWorldTriangles.forceSize_Unsafe(mNbCachedT); mTriangleIndices.forceSize_Unsafe(mNbCachedT); filter.mStaticShapes = false; if(filters.mFilterFlags & PxQueryFlag::eDYNAMIC) filter.mDynamicShapes = true; findTouchedGeometry(userData, DYNAMIC_BOX, mWorldTriangles, mTriangleIndices, mGeomStream, filter, mUserParams, mNbTessellation); updateCachedShapesRegistration(mNbCachedStatic, false); findTouchedObstacles(userObstacles, DYNAMIC_BOX); mNbPartialUpdates++; } } else { //printf("CACHEOUTNS=%d\n", mNbCachedStatic); newCachedBox = true; // Cache BV used for the query mCacheBounds = worldTemporalBox; // Grow the volume a bit. The temporal box here doesn't take sliding & collision response into account. // In bad cases it is possible to eventually touch a portion of space not covered by this volume. Just // in case, we grow the initial volume slightly. Then, additional tests are performed within the loop // to make sure the TBV is always correct. There's a tradeoff between the original (artificial) growth // of the volume, and the number of TBV recomputations performed at runtime... scale(mCacheBounds, PxVec3(mVolumeGrowth)); // scale(mCacheBounds, PxVec3(mVolumeGrowth, 1.0f, mVolumeGrowth)); if(1 && !sideVector.isZero()) { const PxVec3 sn = sideVector.getNormalized(); float dp0 = PxAbs((worldTemporalBox.maximum - worldTemporalBox.minimum).dot(sn)); float dp1 = PxAbs((mCacheBounds.maximum - mCacheBounds.minimum).dot(sn)); dp1 -= dp0; dp1 *= 0.5f * 0.9f; const PxVec3 offset = sn * dp1; // printf("%f %f %f\n", offset.x, offset.y, offset.z); mCacheBounds.minimum += offset; mCacheBounds.maximum += offset; add(mCacheBounds, worldTemporalBox); PX_ASSERT(worldTemporalBox.isInside(mCacheBounds)); } updateCachedShapesRegistration(0, true); // Gather triangles touched by this box. This covers multiple meshes. mWorldTriangles.clear(); mTriangleIndices.clear(); mGeomStream.clear(); // mWorldTriangles.reset(); // mTriangleIndices.reset(); // mGeomStream.reset(); mCachedTriIndexIndex = 0; mCachedTriIndex[0] = mCachedTriIndex[1] = mCachedTriIndex[2] = 0; mNbFullUpdates++; if(filters.mFilterFlags & PxQueryFlag::eSTATIC) filter.mStaticShapes = true; filter.mDynamicShapes = false; findTouchedGeometry(userData, mCacheBounds, mWorldTriangles, mTriangleIndices, mGeomStream, filter, mUserParams, mNbTessellation); mNbCachedStatic = mGeomStream.size(); mNbCachedT = mWorldTriangles.size(); PX_ASSERT(mTriangleIndices.size()==mNbCachedT); filter.mStaticShapes = false; if(filters.mFilterFlags & PxQueryFlag::eDYNAMIC) filter.mDynamicShapes = true; findTouchedGeometry(userData, DYNAMIC_BOX, mWorldTriangles, mTriangleIndices, mGeomStream, filter, mUserParams, mNbTessellation); // We can't early exit when no tris are touched since we also have to handle the boxes updateCachedShapesRegistration(0, false); findTouchedObstacles(userObstacles, DYNAMIC_BOX); mFlags &= ~STF_FIRST_UPDATE; //printf("CACHEOUTNSDONE=%d\n", mNbCachedStatic); } if(mRenderBuffer) { // PT: worldTemporalBox = temporal BV for this frame RenderOutput out(*mRenderBuffer); if(mRenderFlags & PxControllerDebugRenderFlag::eTEMPORAL_BV) { out << gTBVDebugColor; out << DebugBox(getBounds3(worldTemporalBox)); } if(mRenderFlags & PxControllerDebugRenderFlag::eCACHED_BV) { if(newCachedBox) out << PxU32(PxDebugColor::eARGB_RED); else out << PxU32(PxDebugColor::eARGB_GREEN); out << DebugBox(getBounds3(mCacheBounds)); } } } // This is the generic sweep test for all swept volumes, but not character-controller specific bool SweepTest::doSweepTest(const InternalCBData_FindTouchedGeom* userData, InternalCBData_OnHit* userHitData, const UserObstacles& userObstacles, SweptVolume& swept_volume, const PxVec3& direction, const PxVec3& sideVector, PxU32 max_iter, PxU32* nb_collisions, float min_dist, const PxControllerFilters& filters, SweepPass sweepPass, const PxRigidActor*& touchedActorOut, const PxShape*& touchedShapeOut, PxU64 contextID) { // 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; PX_PROFILE_ZONE("CharacterController.doSweepTest", contextID); PX_UNUSED(contextID); bool hasMoved = false; mFlags &= ~(STF_VALIDATE_TRIANGLE_DOWN|STF_TOUCH_OTHER_CCT|STF_TOUCH_OBSTACLE); touchedShapeOut = NULL; touchedActorOut = NULL; mTouchedObstacleHandle = INVALID_OBSTACLE_HANDLE; PxExtendedVec3 currentPosition = swept_volume.mCenter; PxExtendedVec3 targetOrientation = swept_volume.mCenter; targetOrientation += direction; PxU32 NbCollisions = 0; while(max_iter--) { mNbIterations++; // Compute current direction PxVec3 currentDirection = targetOrientation - 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 ? PxExtendedBounds3 temporalBox; swept_volume.computeTemporalBox(*this, temporalBox, currentPosition, currentDirection); // Gather touched geoms updateTouchedGeoms(userData, userObstacles, temporalBox, filters, sideVector); } const float Length = currentDirection.magnitude(); if(Length<=min_dist) //Use <= to handle the case where min_dist is zero. break; currentDirection /= Length; // From Quake2: "if velocity is against the original velocity, stop dead to avoid tiny occilations in sloping corners" if((currentDirection.dot(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 + mUserParams.mContactOffset; if(!CollideGeoms(this, swept_volume, mGeomStream, currentPosition, currentDirection, C, !mUserParams.mOverlapRecovery)) { // no collision found => move to desired position currentPosition = targetOrientation; break; } PX_ASSERT(C.mGeom); // If we reach this point, we must have touched a geom if(mUserParams.mOverlapRecovery && C.mDistance==0.0f) { /* SweptContact C; C.mDistance = 10.0f; CollideGeoms(this, swept_volume, mGeomStream, currentPosition, -currentDirection, C, true); currentPosition -= currentDirection*C.mDistance; C.mDistance = 10.0f; CollideGeoms(this, swept_volume, mGeomStream, currentPosition, currentDirection, C, true); const float DynSkin = mUserParams.mContactOffset; if(C.mDistance>DynSkin) currentPosition += currentDirection*(C.mDistance-DynSkin);*/ const PxVec3 mtd = computeMTD(this, swept_volume, mGeomStream, currentPosition, mUserParams.mContactOffset); NbCollisions++; if(nb_collisions) *nb_collisions = NbCollisions; #ifdef DEBUG_MTD printf("MTD FIXUP: %f %f %f\n", mtd.x - swept_volume.mCenter.x, mtd.y - swept_volume.mCenter.y, mtd.z - swept_volume.mCenter.z); #endif swept_volume.mCenter.x = PxExtended(mtd.x); swept_volume.mCenter.y = PxExtended(mtd.y); swept_volume.mCenter.z = PxExtended(mtd.z); return hasMoved; // currentPosition.x = mtd.x; // currentPosition.y = mtd.y; // currentPosition.z = mtd.z; // continue; } bool preventVerticalMotion = false; bool stopSliding = true; if(C.mGeom->mType==TouchedGeomType::eUSER_BOX || C.mGeom->mType==TouchedGeomType::eUSER_CAPSULE) { if(sweepPass!=SWEEP_PASS_SENSOR) { // We touched a user object, typically another CCT, but can also be a user-defined obstacle // PT: TODO: technically lines marked with (*) shouldn't be here... revisit later const PxObstacle* touchedObstacle = NULL; // (*) ObstacleHandle touchedObstacleHandle = INVALID_OBSTACLE_HANDLE; // if(mValidateCallback) { PxInternalCBData_OnHit* internalData = static_cast(userHitData); // (*) internalData->touchedObstacle = NULL; // (*) internalData->touchedObstacleHandle = INVALID_OBSTACLE_HANDLE; const PxU32 behaviorFlags = userHitCallback(userHitData, C, currentDirection, Length); stopSliding = (behaviorFlags & PxControllerBehaviorFlag::eCCT_SLIDE)==0; // (*) touchedObstacle = internalData->touchedObstacle; // (*) touchedObstacleHandle = internalData->touchedObstacleHandle; } // printf("INTERNAL: %d\n", int(touchedObstacle)); if(sweepPass==SWEEP_PASS_DOWN) { // (*) if(touchedObstacle) { mFlags |= STF_TOUCH_OBSTACLE; mTouchedObstacleHandle = touchedObstacleHandle; if(!gUseLocalSpace) { mTouchedPos = toVec3(touchedObstacle->mPos); } else { mTouchedPosObstacle_World = toVec3(C.mWorldPos); mTouchedPosObstacle_Local = worldToLocal(*touchedObstacle, C.mWorldPos); } } else { mFlags |= STF_TOUCH_OTHER_CCT; } } } } else { const PxShape* touchedShape = reinterpret_cast(C.mGeom->mTGUserData); PX_ASSERT(touchedShape); const PxRigidActor* touchedActor = C.mGeom->mActor; PX_ASSERT(touchedActor); // We touched a normal object if(sweepPass==SWEEP_PASS_DOWN) { mFlags &= ~(STF_TOUCH_OTHER_CCT|STF_TOUCH_OBSTACLE); #ifdef USE_CONTACT_NORMAL_FOR_SLOPE_TEST mFlags |= STF_VALIDATE_TRIANGLE_DOWN; mContactNormalDownPass = C.mWorldNormal; #else // Work out if the shape is attached to a static or dynamic actor. // The slope limit is currently only considered when walking on static actors. // It is ignored for shapes attached attached to dynamics and kinematics. // TODO: 1. should we treat stationary kinematics the same as statics. // 2. should we treat all kinematics the same as statics. // 3. should we treat no kinematics the same as statics. if((touchedActor->getConcreteType() == PxConcreteType::eRIGID_STATIC) && (C.mInternalIndex!=PX_INVALID_U32)) { mFlags |= STF_VALIDATE_TRIANGLE_DOWN; const PxTriangle& touchedTri = mWorldTriangles.getTriangle(C.mInternalIndex); const PxVec3& upDirection = mUserParams.mUpDirection; const float dp0 = touchedTri.verts[0].dot(upDirection); const float dp1 = touchedTri.verts[1].dot(upDirection); const float dp2 = touchedTri.verts[2].dot(upDirection); float dpmin = dp0; dpmin = physx::intrinsics::selectMin(dpmin, dp1); dpmin = physx::intrinsics::selectMin(dpmin, dp2); float dpmax = dp0; dpmax = physx::intrinsics::selectMax(dpmax, dp1); dpmax = physx::intrinsics::selectMax(dpmax, dp2); PxExtendedVec3 cacheCenter; getCenter(mCacheBounds, cacheCenter); const float offset = upDirection.dot(toVec3(cacheCenter)); mTouchedTriMin = dpmin + offset; mTouchedTriMax = dpmax + offset; touchedTri.normal(mContactNormalDownPass); } #endif // Update touched shape in down pass touchedShapeOut = const_cast(touchedShape); touchedActorOut = touchedActor; // mTouchedPos = getShapeGlobalPose(*touchedShape).p; const PxTransform shapeTransform = getShapeGlobalPose(*touchedShape, *touchedActor); const PxVec3 worldPos = toVec3(C.mWorldPos); mTouchedPosShape_World = worldPos; mTouchedPosShape_Local = shapeTransform.transformInv(worldPos); } else if(sweepPass==SWEEP_PASS_SIDE || sweepPass==SWEEP_PASS_SENSOR) { if((touchedActor->getConcreteType() == PxConcreteType::eRIGID_STATIC) && (C.mInternalIndex!=PX_INVALID_U32)) { mFlags |= STF_VALIDATE_TRIANGLE_SIDE; const PxTriangle& touchedTri = mWorldTriangles.getTriangle(C.mInternalIndex); touchedTri.normal(mContactNormalSidePass); // printf("%f | %f | %f\n", mContactNormalSidePass.x, mContactNormalSidePass.y, mContactNormalSidePass.z); if(mUserParams.mPreventVerticalSlidingAgainstCeiling && mContactNormalSidePass.dot(mUserParams.mUpDirection)<0.0f) preventVerticalMotion = true; } } if(sweepPass!=SWEEP_PASS_SENSOR) // if(mValidateCallback) { const PxU32 behaviorFlags = shapeHitCallback(userHitData, C, currentDirection, Length); stopSliding = (behaviorFlags & PxControllerBehaviorFlag::eCCT_SLIDE)==0; // (*) } } if(sweepPass==SWEEP_PASS_DOWN && !stopSliding) { // 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(!NbCollisions) max_iter += 9; // max_iter += 1; } NbCollisions++; // mContactPointHeight = (float)C.mWorldPos[mUserParams.mUpDirection]; // UBI mContactPointHeight = toVec3(C.mWorldPos).dot(mUserParams.mUpDirection); // UBI const float DynSkin = mUserParams.mContactOffset; if(C.mDistance>DynSkin/*+0.01f*/) currentPosition += currentDirection*(C.mDistance-DynSkin); // DE6513 /* else if(sweepPass==SWEEP_PASS_SIDE) { // Might be better to do a proper sweep pass here, in the opposite direction currentPosition += currentDirection*(C.mDistance-DynSkin); }*/ //~DE6513 PxVec3 WorldNormal = C.mWorldNormal; if(preventVerticalMotion || ((mFlags & STF_WALK_EXPERIMENT) && (mUserParams.mNonWalkableMode!=PxControllerNonWalkableMode::ePREVENT_CLIMBING_AND_FORCE_SLIDING))) { // Make sure the auto-step doesn't bypass this ! // PT: cancel out normal compo // WorldNormal[mUserParams.mUpDirection]=0.0f; // WorldNormal.normalize(); PxVec3 normalCompo, tangentCompo; Ps::decomposeVector(normalCompo, tangentCompo, WorldNormal, mUserParams.mUpDirection); WorldNormal = tangentCompo; WorldNormal.normalize(); } const float Bump = 0.0f; // ### doesn't work when !=0 because of Quake2 hack! const float Friction = 1.0f; collisionResponse(targetOrientation, currentPosition, currentDirection, WorldNormal, Bump, Friction, (mFlags & STF_NORMALIZE_RESPONSE)!=0); } 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; } // ### have a return code to tell if we really moved or not // Using swept code & direct position update (no physics engine) // This function is the generic character controller logic, valid for all swept volumes PxControllerCollisionFlags SweepTest::moveCharacter( const InternalCBData_FindTouchedGeom* userData, InternalCBData_OnHit* userHitData, SweptVolume& volume, const PxVec3& direction, const UserObstacles& userObstacles, float min_dist, const PxControllerFilters& filters, bool constrainedClimbingMode, bool standingOnMoving, const PxRigidActor*& touchedActor, const PxShape*& touchedShape, PxU64 contextID) { PX_PROFILE_ZONE("CharacterController.moveCharacter", contextID); PX_UNUSED(contextID); bool standingOnMovingUp = standingOnMoving; mFlags &= ~STF_HIT_NON_WALKABLE; PxControllerCollisionFlags CollisionFlags = PxControllerCollisionFlags(0); const PxU32 maxIter = MAX_ITER; // 1 for "collide and stop" const PxU32 maxIterSides = maxIter; const PxU32 maxIterDown = ((mFlags & STF_WALK_EXPERIMENT) && mUserParams.mNonWalkableMode==PxControllerNonWalkableMode::ePREVENT_CLIMBING_AND_FORCE_SLIDING) ? maxIter : 1; // const PxU32 maxIterDown = 1; // ### this causes the artificial gap on top of chars float stepOffset = mUserParams.mStepOffset; // Default step offset can be cancelled in some cases. // Save initial height const PxVec3& upDirection = mUserParams.mUpDirection; const PxExtended originalHeight = volume.mCenter.dot(upDirection); const PxExtended originalBottomPoint = originalHeight - PxExtended(volume.mHalfHeight); // UBI // TEST! Disable auto-step when flying. Not sure this is really useful. // if(direction[upDirection]>0.0f) const float dir_dot_up = direction.dot(upDirection); //printf("%f\n", dir_dot_up); if(dir_dot_up>0.0f) { mFlags |= STF_IS_MOVING_UP; // PT: this makes it fail on a platform moving up when jumping // However if we don't do that a jump when moving up a slope doesn't work anymore! // Not doing this also creates jittering when a capsule CCT jumps against another capsule CCT if(!standingOnMovingUp) // PT: if we're standing on something moving up it's safer to do the up motion anyway, even though this won't work well before we add the flag in TA13542 { // static int count=0; printf("Cancelling step offset... %d\n", count++); stepOffset = 0.0f; } } else { mFlags &= ~STF_IS_MOVING_UP; } // 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 PxVec3 UpVector(0.0f, 0.0f, 0.0f); PxVec3 DownVector(0.0f, 0.0f, 0.0f); PxVec3 normal_compo, tangent_compo; Ps::decomposeVector(normal_compo, tangent_compo, direction, upDirection); // if(direction[upDirection]<0.0f) if(dir_dot_up<=0.0f) // DownVector[upDirection] = direction[upDirection]; DownVector = normal_compo; else // UpVector[upDirection] = direction[upDirection]; UpVector = normal_compo; // PxVec3 SideVector = direction; // SideVector[upDirection] = 0.0f; PxVec3 SideVector = tangent_compo; // If the side motion is zero, i.e. if the character is not really moving, disable auto-step. // This is important to prevent the CCT from automatically climbing on small objects that move // against it. We should climb over those only if there's a valid side motion from the player. const bool sideVectorIsZero = !standingOnMovingUp && Ps::isAlmostZero(SideVector); // We can't use PxVec3::isZero() safely with arbitrary up vectors // #### however if we do this the up pass is disabled, with bad consequences when the CCT is on a dynamic object!! // ### this line makes it possible to push other CCTs by jumping on them // const bool sideVectorIsZero = false; // printf("sideVectorIsZero: %d\n", sideVectorIsZero); // if(!SideVector.isZero()) if(!sideVectorIsZero) // UpVector[upDirection] += stepOffset; UpVector += upDirection*stepOffset; // printf("stepOffset: %f\n", stepOffset); // ==========[ Initial volume query ]=========================== // PT: the main difference between this initial query and subsequent ones is that we use the // full direction vector here, not the components along each axis. So there is a good chance // that this initial query will contain all the motion we need, and thus subsequent queries // will be skipped. { PxExtendedBounds3 temporalBox; volume.computeTemporalBox(*this, temporalBox, volume.mCenter, direction); // Gather touched geoms updateTouchedGeoms(userData, userObstacles, temporalBox, filters, SideVector); } // ==========[ UP PASS ]=========================== mCachedTriIndexIndex = 0; const bool performUpPass = true; PxU32 NbCollisions=0; PxU32 maxIterUp; if(mUserParams.mPreventVerticalSlidingAgainstCeiling) maxIterUp = 1; else maxIterUp = Ps::isAlmostZero(SideVector) ? maxIter : 1; if(performUpPass) { // printf("%f | %f | %f\n", UpVector.x, UpVector.y, UpVector.z); // 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; // PT: actually I think the previous comment is wrong. It's not only needed for auto-stepping: when the character // jumps there's a legit up motion and the lack of callback in that case could need some object can't be pushed // by the character's 'head' (for example). So I now think it's better to use the callback all the time, and // let users figure out what to do using the available state (like "isMovingUp", etc). // mValidateCallback = true; // In the walk-experiment we explicitly want to ban any up motions, to avoid characters climbing slopes they shouldn't climb. // So let's bypass the whole up pass. if(!(mFlags & STF_WALK_EXPERIMENT)) { // ### maxIter here seems to "solve" the V bug if(doSweepTest(userData, userHitData, userObstacles, volume, UpVector, SideVector, maxIterUp, &NbCollisions, min_dist, filters, SWEEP_PASS_UP, touchedActor, touchedShape, contextID)) { if(NbCollisions) { CollisionFlags |= PxControllerCollisionFlag::eCOLLISION_UP; // Clamp step offset to make sure we don't undo more than what we did float Delta = float(volume.mCenter.dot(upDirection) - originalHeight); if(Delta(volume).mRadius; const float sideM = SideVector.magnitude(); if(sideM originalBottomPoint + PxExtended(stepOffset)) { mFlags |= STF_HIT_NON_WALKABLE; if(!(mFlags & STF_WALK_EXPERIMENT)) return CollisionFlags; // printf("Contrained\n"); } } //~constrainedClimbingMode } } } //printf("AD:%.2f %.2f %.2f NS=%d\n", volume.mCenter.x, volume.mCenter.y, volume.mCenter.z, mNbCachedStatic); // printf("%d\n", mTouchOtherCCT); // 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(mUserParams.mHandleSlope && mValidateTriangle && direction[upDirection]<0.0f) // if(mUserParams.mHandleSlope && !mTouchOtherCCT && !mTouchObstacle && mValidateTriangle && dir_dot_up<0.0f) if(mUserParams.mHandleSlope && !(mFlags & (STF_TOUCH_OTHER_CCT|STF_TOUCH_OBSTACLE)) && (mFlags & STF_VALIDATE_TRIANGLE_DOWN) && dir_dot_up<=0.0f) { PxVec3 Normal; #ifdef USE_CONTACT_NORMAL_FOR_SLOPE_TEST Normal = mContactNormalDownPass; #else //mTouchedTriangle.normal(Normal); Normal = mContactNormalDownPass; #endif const float touchedTriHeight = float(PxExtended(mTouchedTriMax) - originalBottomPoint); /* if(touchedTriHeight>mUserParams.mStepOffset) { if(constrainedClimbingMode && mContactPointHeight > originalBottomPoint + stepOffset) { mFlags |= STF_HIT_NON_WALKABLE; if(!(mFlags & STF_WALK_EXPERIMENT)) return CollisionFlags; } }*/ if(touchedTriHeight>mUserParams.mStepOffset && testSlope(Normal, upDirection, mUserParams.mSlopeLimit)) { mFlags |= STF_HIT_NON_WALKABLE; // Early exit if we're going to run this again anyway... if(!(mFlags & STF_WALK_EXPERIMENT)) return CollisionFlags; /* CatchScene()->GetRenderer()->AddLine(mTouchedTriangle.mVerts[0], mTouched.mVerts[1], ARGB_YELLOW); CatchScene()->GetRenderer()->AddLine(mTouchedTriangle.mVerts[0], mTouched.mVerts[2], ARGB_YELLOW); CatchScene()->GetRenderer()->AddLine(mTouchedTriangle.mVerts[1], mTouched.mVerts[2], ARGB_YELLOW); */ // ==========[ WALK EXPERIMENT ]=========================== mFlags |= STF_NORMALIZE_RESPONSE; const PxExtended tmp = volume.mCenter.dot(upDirection); float Delta = tmp > originalHeight ? float(tmp - originalHeight) : 0.0f; Delta += fabsf(direction.dot(upDirection)); float Recover = Delta; NbCollisions=0; const float MD = Recover < min_dist ? Recover/float(maxIter) : min_dist; PxVec3 RecoverPoint(0,0,0); RecoverPoint = -upDirection*Recover; // PT: we pass "SWEEP_PASS_UP" for compatibility with previous code, but it's technically wrong (this is a 'down' pass) if(doSweepTest(userData, userHitData, userObstacles, volume, RecoverPoint, SideVector, maxIter, &NbCollisions, MD, filters, SWEEP_PASS_UP, touchedActor, touchedShape, contextID)) { // 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 &= ~PxControllerCollisionFlag::eCOLLISION_DOWN; } mFlags &= ~STF_NORMALIZE_RESPONSE; } } } return CollisionFlags; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // This is an interface between NX users and the internal character controller module. #include "characterkinematic/PxControllerBehavior.h" #include "PxActor.h" #include "PxScene.h" #include "CctInternalStructs.h" #include "CctBoxController.h" #include "CctCapsuleController.h" #include "CctCharacterControllerManager.h" #include "CctObstacleContext.h" // PT: we use a local class instead of making "Controller" a PxQueryFilterCallback, since it would waste more memory. // Ideally we'd have a C-style callback and a user-data pointer, instead of being forced to create a class. class ControllerFilter : public PxQueryFilterCallback { public: PxQueryHitType::Enum preFilter(const PxFilterData& filterData, const PxShape* shape, const PxRigidActor* actor, PxHitFlags& queryFlags) { // PT: ignore triggers if(shape->getFlags() & physx::PxShapeFlag::eTRIGGER_SHAPE) return PxQueryHitType::eNONE; // PT: we want to discard our own internal shapes only if(mShapeHashSet->contains(const_cast(shape))) return PxQueryHitType::eNONE; // PT: otherwise we revert to the user-callback, if it exists, and if users enabled that call if(mUserFilterCallback && (mUserFilterFlags | PxQueryFlag::ePREFILTER)) return mUserFilterCallback->preFilter(filterData, shape, actor, queryFlags); return PxQueryHitType::eBLOCK; } PxQueryHitType::Enum postFilter(const PxFilterData& filterData, const PxQueryHit& hit) { // PT: we may get called if users have asked for such a callback if(mUserFilterCallback && (mUserFilterFlags | PxQueryFlag::ePOSTFILTER)) return mUserFilterCallback->postFilter(filterData, hit); PX_ASSERT(0); // PT: otherwise we shouldn't have been called return PxQueryHitType::eNONE; } Ps::HashSet* mShapeHashSet; PxQueryFilterCallback* mUserFilterCallback; PxQueryFlags mUserFilterFlags; }; bool Controller::filterTouchedShape(const PxControllerFilters& filters) { PxQueryFlags filterFlags = PxQueryFlag::eDYNAMIC|PxQueryFlag::ePREFILTER; PxQueryFilterData filterData(filters.mFilterData ? *filters.mFilterData : PxFilterData(), filterFlags); PxHitFlags hitFlags = PxHitFlags(0); if(filters.mFilterCallback && (filters.mFilterFlags | PxQueryFlag::ePREFILTER)) { PxQueryHitType::Enum retVal = filters.mFilterCallback->preFilter(filterData.data, mCctModule.mTouchedShape.get(), mCctModule.mTouchedActor.get(), hitFlags); if(retVal != PxQueryHitType::eNONE) return true; else return false; } return true; } void Controller::findTouchedObject(const PxControllerFilters& filters, const PxObstacleContext* obstacleContext, const PxVec3& upDirection) { PX_ASSERT(!mCctModule.mTouchedShape && (mCctModule.mTouchedObstacleHandle == INVALID_OBSTACLE_HANDLE)); // PT: the CCT works perfectly on statics without this extra mechanism, so we only raycasts against dynamics. // The pre-filter callback is used to filter out our own proxy actor shapes. We need to make sure our own filter // doesn't disturb the user-provided filter(s). // PT: for starter, if user doesn't want to collide against dynamics, we can skip the whole thing if(filters.mFilterFlags & PxQueryFlag::eDYNAMIC) { ControllerFilter preFilter; preFilter.mShapeHashSet = &mManager->mCCTShapes; preFilter.mUserFilterCallback = filters.mFilterCallback; preFilter.mUserFilterFlags = filters.mFilterFlags; // PT: for our own purpose we just want dynamics & pre-filter PxQueryFlags filterFlags = PxQueryFlag::eDYNAMIC|PxQueryFlag::ePREFILTER; // PT: but we may need the post-filter callback as well if users want it if(filters.mFilterFlags & PxQueryFlag::ePOSTFILTER) filterFlags |= PxQueryFlag::ePOSTFILTER; PxQueryFilterData filterData(filters.mFilterData ? *filters.mFilterData : PxFilterData(), filterFlags); const PxF32 probeLength = getHalfHeightInternal(); // Distance to feet const PxF32 extra = 0.0f;//probeLength * 0.1f; const PxVec3 rayOrigin = toVec3(mPosition); PxRaycastBuffer hit; hit.block.distance = FLT_MAX; if(mScene->raycast(rayOrigin, -upDirection, probeLength+extra, hit, PxHitFlags(0), filterData, &preFilter)) { // copy touching hit to blocking so that the rest of the code works with .block hit.block = hit.getAnyHit(0); PX_ASSERT(hit.block.shape); PX_ASSERT(hit.block.actor); PX_ASSERT(hit.block.distance<=probeLength+extra); mCctModule.mTouchedShape = hit.block.shape; mCctModule.mTouchedActor = hit.block.actor; // mCctModule.mTouchedPos = getShapeGlobalPose(*hit.shape).p - upDirection*(probeLength-hit.distance); // PT: we only care about the up delta here const PxTransform shapeTransform = getShapeGlobalPose(*hit.block.shape, *hit.block.actor); mCctModule.mTouchedPosShape_World = PxVec3(0) - upDirection*(probeLength-hit.block.distance); mCctModule.mTouchedPosShape_Local = shapeTransform.transformInv(PxVec3(0)); mPreviousSceneTimestamp = mScene->getTimestamp()-1; // PT: just make sure cached timestamp is different } if(obstacleContext) { const ObstacleContext* obstacles = static_cast(obstacleContext); PxRaycastHit obstacleHit; ObstacleHandle obstacleHandle; const PxObstacle* touchedObstacle = obstacles->raycastSingle(obstacleHit, rayOrigin, -upDirection, probeLength+extra, obstacleHandle); // printf("Touched raycast obstacle: %d\n", int(touchedObstacle)); if(touchedObstacle && obstacleHit.distancemPos) - upDirection*(probeLength-obstacleHit.distance); } else { // PT: we only care about the up delta here mCctModule.mTouchedPosObstacle_World = PxVec3(0) - upDirection*(probeLength-obstacleHit.distance); mCctModule.mTouchedPosObstacle_Local = worldToLocal(*touchedObstacle, PxExtendedVec3(0,0,0)); } } } } } bool Controller::rideOnTouchedObject(SweptVolume& volume, const PxVec3& upDirection, PxVec3& disp, const PxObstacleContext* obstacleContext) { PX_ASSERT(mCctModule.mTouchedShape || (mCctModule.mTouchedObstacleHandle != INVALID_OBSTACLE_HANDLE)); bool standingOnMoving = false; bool canDoUpdate = true; // Always true on obstacles PxU32 behaviorFlags = 0; // Default on shapes PxVec3 delta(0); float timeCoeff = 1.0f; if(mCctModule.mTouchedShape) { // PT: riding on a shape // PT: it is important to skip this stuff for static meshes, // otherwise accuracy issues create bugs like TA14007. const PxRigidActor& rigidActor = *mCctModule.mTouchedActor.get(); if(rigidActor.getConcreteType()!=PxConcreteType::eRIGID_STATIC) { // PT: we only do the update when the timestamp has changed, otherwise "delta" will be zero // even if the underlying shape is moving. const PxU32 timestamp = mScene->getTimestamp(); // printf("TimeStamp: %d\n", timestamp); canDoUpdate = timestamp!=mPreviousSceneTimestamp; if(canDoUpdate) { mPreviousSceneTimestamp = timestamp; timeCoeff = computeTimeCoeff(); if(mBehaviorCallback) behaviorFlags = mBehaviorCallback->getBehaviorFlags(*mCctModule.mTouchedShape.get(), *mCctModule.mTouchedActor.get()); // delta = getShapeGlobalPose(*mCctModule.mTouchedShape).p - mCctModule.mTouchedPos; const PxTransform shapeTransform = getShapeGlobalPose(*mCctModule.mTouchedShape.get(), rigidActor); const PxVec3 posPreviousFrame = mCctModule.mTouchedPosShape_World; const PxVec3 posCurrentFrame = shapeTransform.transform(mCctModule.mTouchedPosShape_Local); delta = posCurrentFrame - posPreviousFrame; } } } else { // PT: riding on an obstacle behaviorFlags = PxControllerBehaviorFlag::eCCT_CAN_RIDE_ON_OBJECT; // Default on obstacles timeCoeff = computeTimeCoeff(); const PxObstacle* touchedObstacle = obstacleContext->getObstacleByHandle(mCctModule.mTouchedObstacleHandle); PX_ASSERT(touchedObstacle); if(mBehaviorCallback) behaviorFlags = mBehaviorCallback->getBehaviorFlags(*touchedObstacle); if(!gUseLocalSpace) { delta = toVec3(touchedObstacle->mPos) - mCctModule.mTouchedPos; } else { PxVec3 posPreviousFrame = mCctModule.mTouchedPosObstacle_World; PxVec3 posCurrentFrame = localToWorld(*touchedObstacle, mCctModule.mTouchedPosObstacle_Local); delta = posCurrentFrame - posPreviousFrame; } } if(canDoUpdate && !(behaviorFlags & PxControllerBehaviorFlag::eCCT_USER_DEFINED_RIDE)) { // PT: amazingly enough even isAlmostZero doesn't solve this one. // Moving on a static mesh sometimes produces delta bigger than 1e-6f! // This may also explain the drift on some rotating platforms. It looks // like this delta computation is not very accurate. // standingOnMoving = !delta.isZero(); standingOnMoving = !Ps::isAlmostZero(delta); mCachedStandingOnMoving = standingOnMoving; //printf("%f %f %f\n", delta.x, delta.y, delta.z); if(standingOnMoving) { const float dir_dot_up = delta.dot(upDirection); const bool deltaMovingUp = dir_dot_up>0.0f; PxVec3 deltaUpDisp, deltaSideDisp; Ps::decomposeVector(deltaUpDisp, deltaSideDisp, delta, upDirection); if(deltaMovingUp) { volume.mCenter.x += PxExtended(deltaUpDisp.x); volume.mCenter.y += PxExtended(deltaUpDisp.y); volume.mCenter.z += PxExtended(deltaUpDisp.z); } else { disp += deltaUpDisp; } if(behaviorFlags & PxControllerBehaviorFlag::eCCT_CAN_RIDE_ON_OBJECT) disp += deltaSideDisp; } // printf("delta in: %f %f %f (%f)\n", delta.x, delta.y, delta.z, 1.0f/timeCoeff); mDeltaXP = delta * timeCoeff; } else { standingOnMoving = mCachedStandingOnMoving; } // mDelta = delta; return standingOnMoving; } PxControllerCollisionFlags Controller::move(SweptVolume& volume, const PxVec3& originalDisp, PxF32 minDist, PxF32 elapsedTime, const PxControllerFilters& filters, const PxObstacleContext* obstacleContext, bool constrainedClimbingMode) { const bool lockWrite = mManager->mLockingEnabled; if(lockWrite) mWriteLock.lock(); mGlobalTime += PxF64(elapsedTime); // Init CCT with per-controller settings RenderBuffer* renderBuffer = mManager->mRenderBuffer; const PxU32 debugRenderFlags = mManager->mDebugRenderingFlags; mCctModule.mRenderBuffer = renderBuffer; mCctModule.mRenderFlags = debugRenderFlags; mCctModule.mUserParams = mUserParams; mCctModule.mFlags |= STF_FIRST_UPDATE; mCctModule.mUserParams.mMaxEdgeLength2 = mManager->mMaxEdgeLength * mManager->mMaxEdgeLength; mCctModule.mUserParams.mTessellation = mManager->mTessellation; mCctModule.mUserParams.mOverlapRecovery = mManager->mOverlapRecovery; mCctModule.mUserParams.mPreciseSweeps = mManager->mPreciseSweeps; mCctModule.mUserParams.mPreventVerticalSlidingAgainstCeiling = mManager->mPreventVerticalSlidingAgainstCeiling; mCctModule.resetStats(); const PxVec3& upDirection = mUserParams.mUpDirection; /////////// PxVec3 disp = originalDisp + mOverlapRecover; mOverlapRecover = PxVec3(0.0f); bool standingOnMoving = false; // PT: whether the CCT is currently standing on a moving object //printf("Touched shape: %d\n", int(mCctModule.mTouchedShape)); //standingOnMoving=true; // printf("Touched obstacle: %d\n", int(mCctModule.mTouchedObstacle)); if(mCctModule.mTouchedActor && mCctModule.mTouchedShape) { PxU32 nbShapes = mCctModule.mTouchedActor->getNbShapes(); bool found = false; for(PxU32 i=0;igetShapes(&shape, 1, i); if(mCctModule.mTouchedShape==shape) { found = true; break; } } if(!found) { mCctModule.mTouchedActor = NULL; mCctModule.mTouchedShape = NULL; } else { // check if we are still in the same scene if(mCctModule.mTouchedActor->getScene() != mScene) { mCctModule.mTouchedShape = NULL; mCctModule.mTouchedActor = NULL; } else { // check if the shape still does have the sq flag if(!(mCctModule.mTouchedShape->getFlags() & PxShapeFlag::eSCENE_QUERY_SHAPE)) { mCctModule.mTouchedShape = NULL; mCctModule.mTouchedActor = NULL; } else { // invoke the CCT filtering for the shape if(!filterTouchedShape(filters)) { mCctModule.mTouchedShape = NULL; mCctModule.mTouchedActor = NULL; } } } } } if(!mCctModule.mTouchedShape && (mCctModule.mTouchedObstacleHandle == INVALID_OBSTACLE_HANDLE)) findTouchedObject(filters, obstacleContext, upDirection); if(mCctModule.mTouchedShape || (mCctModule.mTouchedObstacleHandle != INVALID_OBSTACLE_HANDLE)) { standingOnMoving = rideOnTouchedObject(volume, upDirection, disp, obstacleContext); } else { mCachedStandingOnMoving = false; mDeltaXP = PxVec3(0.0f); } // printf("standingOnMoving: %d\n", standingOnMoving); /////////// Ps::Array& boxUserData = mManager->mBoxUserData; Ps::Array& boxes = mManager->mBoxes; Ps::Array& capsuleUserData = mManager->mCapsuleUserData; Ps::Array& capsules = mManager->mCapsules; PX_ASSERT(!boxUserData.size()); PX_ASSERT(!boxes.size()); PX_ASSERT(!capsuleUserData.size()); PX_ASSERT(!capsules.size()); { PX_PROFILE_ZONE("CharacterController.filterCandidateControllers", getContextId()); // Experiment - to do better const PxU32 nbControllers = mManager->getNbControllers(); Controller** controllers = mManager->getControllers(); for(PxU32 i=0;ifilter(*getPxController(), *currentController->getPxController()); if(keepController) { if(currentController->mType==PxControllerShapeType::eBOX) { // PT: TODO: optimize this BoxController* BC = static_cast(currentController); PxExtendedBox obb; BC->getOBB(obb); boxes.pushBack(obb); #ifdef REMOVED if(renderBuffer /*&& (debugRenderFlags & PxControllerDebugRenderFlag::eOBSTACLES)*/) { RenderOutput out(*renderBuffer); out << gCCTBoxDebugColor; out << PxTransform(toVec3(obb.center), obb.rot); out << DebugBox(obb.extents, true); } #endif const size_t code = encodeUserObject(i, USER_OBJECT_CCT); boxUserData.pushBack(reinterpret_cast(code)); } else if(currentController->mType==PxControllerShapeType::eCAPSULE) { CapsuleController* CC = static_cast(currentController); // PT: TODO: optimize this PxExtendedCapsule worldCapule; CC->getCapsule(worldCapule); capsules.pushBack(worldCapule); const size_t code = encodeUserObject(i, USER_OBJECT_CCT); capsuleUserData.pushBack(reinterpret_cast(code)); } else PX_ASSERT(0); } } } const ObstacleContext* obstacles = NULL; if(obstacleContext) { obstacles = static_cast(obstacleContext); // PT: TODO: optimize this const PxU32 nbExtraBoxes = obstacles->mBoxObstacles.size(); for(PxU32 i=0;imBoxObstacles[i].mData; PxExtendedBox extraBox; extraBox.center = userBoxObstacle.mPos; extraBox.extents = userBoxObstacle.mHalfExtents; extraBox.rot = userBoxObstacle.mRot; boxes.pushBack(extraBox); const size_t code = encodeUserObject(i, USER_OBJECT_BOX_OBSTACLE); boxUserData.pushBack(reinterpret_cast(code)); if(renderBuffer && (debugRenderFlags & PxControllerDebugRenderFlag::eOBSTACLES)) { RenderOutput out(*renderBuffer); out << gObstacleDebugColor; out << PxTransform(toVec3(userBoxObstacle.mPos), userBoxObstacle.mRot); out << DebugBox(userBoxObstacle.mHalfExtents, true); } } const PxU32 nbExtraCapsules = obstacles->mCapsuleObstacles.size(); for(PxU32 i=0;imCapsuleObstacles[i].mData; PxExtendedCapsule extraCapsule; const PxVec3 capsuleAxis = userCapsuleObstacle.mRot.getBasisVector0() * userCapsuleObstacle.mHalfHeight; extraCapsule.p0 = PxExtendedVec3( userCapsuleObstacle.mPos.x - PxExtended(capsuleAxis.x), userCapsuleObstacle.mPos.y - PxExtended(capsuleAxis.y), userCapsuleObstacle.mPos.z - PxExtended(capsuleAxis.z)); extraCapsule.p1 = PxExtendedVec3( userCapsuleObstacle.mPos.x + PxExtended(capsuleAxis.x), userCapsuleObstacle.mPos.y + PxExtended(capsuleAxis.y), userCapsuleObstacle.mPos.z + PxExtended(capsuleAxis.z)); extraCapsule.radius = userCapsuleObstacle.mRadius; capsules.pushBack(extraCapsule); const size_t code = encodeUserObject(i, USER_OBJECT_CAPSULE_OBSTACLE); capsuleUserData.pushBack(reinterpret_cast(code)); if(renderBuffer && (debugRenderFlags & PxControllerDebugRenderFlag::eOBSTACLES)) { RenderOutput out(*renderBuffer); out << gObstacleDebugColor; out.outputCapsule(userCapsuleObstacle.mRadius, userCapsuleObstacle.mHalfHeight, PxTransform(toVec3(userCapsuleObstacle.mPos), userCapsuleObstacle.mRot)); } } } UserObstacles userObstacles; const PxU32 nbBoxes = boxes.size(); userObstacles.mNbBoxes = nbBoxes; userObstacles.mBoxes = nbBoxes ? boxes.begin() : NULL; userObstacles.mBoxUserData = nbBoxes ? boxUserData.begin() : NULL; const PxU32 nbCapsules = capsules.size(); userObstacles.mNbCapsules = nbCapsules; userObstacles.mCapsules = nbCapsules ? capsules.begin() : NULL; userObstacles.mCapsuleUserData = nbCapsules ? capsuleUserData.begin() : NULL; PxInternalCBData_OnHit userHitData; userHitData.controller = this; userHitData.obstacles = obstacles; /////////// PxControllerCollisionFlags collisionFlags = PxControllerCollisionFlags(0); PxInternalCBData_FindTouchedGeom findGeomData; findGeomData.scene = mScene; findGeomData.renderBuffer = renderBuffer; findGeomData.cctShapeHashSet = &mManager->mCCTShapes; mCctModule.mFlags &= ~STF_WALK_EXPERIMENT; // store new touched actor/shape. Then set new actor/shape to avoid register/unregister for same objects const PxRigidActor* touchedActor = NULL; const PxShape* touchedShape = NULL; PxExtendedVec3 Backup = volume.mCenter; collisionFlags = mCctModule.moveCharacter(&findGeomData, &userHitData, volume, disp, userObstacles, minDist, filters, constrainedClimbingMode, standingOnMoving, touchedActor, touchedShape, getContextId()); if(mCctModule.mFlags & STF_HIT_NON_WALKABLE) { // A bit slow, but everything else I tried was less convincing... mCctModule.mFlags |= STF_WALK_EXPERIMENT; volume.mCenter = Backup; PxVec3 xpDisp; if(mUserParams.mNonWalkableMode==PxControllerNonWalkableMode::ePREVENT_CLIMBING_AND_FORCE_SLIDING) { PxVec3 tangent_compo; Ps::decomposeVector(xpDisp, tangent_compo, disp, upDirection); } else xpDisp = disp; collisionFlags = mCctModule.moveCharacter(&findGeomData, &userHitData, volume, xpDisp, userObstacles, minDist, filters, constrainedClimbingMode, standingOnMoving, touchedActor, touchedShape, getContextId()); mCctModule.mFlags &= ~STF_WALK_EXPERIMENT; } mCctModule.mTouchedActor = touchedActor; mCctModule.mTouchedShape = touchedShape; mCollisionFlags = collisionFlags; // Copy results back mPosition = volume.mCenter; // Update kinematic actor if(mKineActor) { const PxVec3 delta = Backup - volume.mCenter; const PxF32 deltaM2 = delta.magnitudeSquared(); if(deltaM2!=0.0f) { PxTransform targetPose = mKineActor->getGlobalPose(); targetPose.p = toVec3(mPosition); targetPose.q = mUserParams.mQuatFromUp; mKineActor->setKinematicTarget(targetPose); } } mManager->resetObstaclesBuffers(); if (lockWrite) mWriteLock.unlock(); return collisionFlags; } PxControllerCollisionFlags BoxController::move(const PxVec3& disp, PxF32 minDist, PxF32 elapsedTime, const PxControllerFilters& filters, const PxObstacleContext* obstacles) { PX_PROFILE_ZONE("CharacterController.move", getContextId()); PX_SIMD_GUARD; // Create internal swept box SweptBox sweptBox; sweptBox.mCenter = mPosition; sweptBox.mExtents = PxVec3(mHalfHeight, mHalfSideExtent, mHalfForwardExtent); sweptBox.mHalfHeight = mHalfHeight; // UBI return Controller::move(sweptBox, disp, minDist, elapsedTime, filters, obstacles, false); } PxControllerCollisionFlags CapsuleController::move(const PxVec3& disp, PxF32 minDist, PxF32 elapsedTime, const PxControllerFilters& filters, const PxObstacleContext* obstacles) { PX_PROFILE_ZONE("CharacterController.move", getContextId()); PX_SIMD_GUARD; // Create internal swept capsule SweptCapsule sweptCapsule; sweptCapsule.mCenter = mPosition; sweptCapsule.mRadius = mRadius; sweptCapsule.mHeight = mHeight; sweptCapsule.mHalfHeight = mHeight*0.5f + mRadius; // UBI return Controller::move(sweptCapsule, disp, minDist, elapsedTime, filters, obstacles, mClimbingMode==PxCapsuleClimbingMode::eCONSTRAINED); }