NVIDIAGameWorks / PhysX

NVIDIA PhysX SDK
Other
3.16k stars 800 forks source link

Character controller and capsules #450

Open tommitytom opened 3 years ago

tommitytom commented 3 years ago

We've been using the PhysX CCT in our game and it has proven to be performant and capable - but we notice through testing (and in the doumentation) that capsule and sphere colliders are not respected by the slope limit. Our artists have used capsule colliders pretty extensively for trees, so our character is able to seemingly climb vertically up trees! The documentation suggests disabling the slopeLimit and using invisibleWallHeight instead, but that didn't seem to have any effect. Am I missing something here? What is the best practice when dealing with capsule colliders and a CCT?

PierreTerdiman commented 3 years ago

Yes, the slope limit doesn't work for rounded primitives, since their impact normal varies depending on where you hit them. So their "slope" is not clearly defined.

IIRC invisible walls are only implemented for triangulated primitives, so indeed, that would not work either for spheres and capsules.

I'm not sure we have a "best practice" yet for this case, I don't recall anybody else asking about this. This is a valid case and it looks like a limitation of the system indeed, but nobody reported it before.

There are probably a number of options here but nothing immediate / simple. At the end of the day the last-resort option is just to add invisible walls manually in the scene. The slope limit (and the automatic invisible walls features) are both based on a binary decision (the slope), which is often not enough anyway. Sometimes you cannot tweak the slopes for artistic reasons. Sometimes you're dealing with procedural geometry whose slopes are not always easy to control. Sometimes you just want plain invisible walls for gameplay reasons, unrelated to how the surrounding geometry looks like. Etc. So in practice people still put manual walls in the levels here and there. It could be tedious to do that around a huge amount of trees though.

You could perhaps use box obstacles (PxBoxObstacle) around these capsules. But if you have a lot of them that might create performance issues.

Something that could work is hack the code in outputCapsuleToStream(), so that it outputs a box instead of a capsule to the collision cache/stream. So you compute the (oriented) bounding box around the incoming capsule, and then you replicate (or call) outputBoxToStream() from outputCapsuleToStream(). We treat boxes as triangle meshes, so the slope etc should start to work then. The Gu::computeBoxAroundCapsule() function could be useful here. I can perhaps give it a try if you don't manage to make it work.

Any of these options sounds good to you so far?

elderkeltir commented 3 years ago

for your particular case with trees you can try to use https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/Manual/CharacterControllers.html#behavior-callback in callback function you have to distinguish that the touched shape is an actual tree and if so - return PxControllerBehaviorFlag::eCCT_SLIDE. if this will work out - it's the easiest you can do without digging into cct code. From my humble experience with kinematic cct provided by physx - it's not out-of-the-box everything-included production-ready solution for AAA games. It's more like base which you can use to build what you need in your project. Get rid of anything you don't need, fix few bugs here and there, spend few months on testing and polishing it - and here you go xD So if you chose to rely on it - get ready to dig into it. ps: slope limit also isn't implemented for dynamic actors.

tommitytom commented 3 years ago

You could perhaps use box obstacles (PxBoxObstacle) around these capsules. But if you have a lot of them that might create performance issues.

I noticed in the documentation that there is a capsule obstacle (At the time of writing the character controller supports box and capsule PxObstacle objects, namely PxBoxObstacle and PxCapsuleObstacle). I followed the instructions but they seemingly had no effect - I tried box obstacles and they didn't seem to have effect either. I enabled debug rendering for the character controller, and I can see debug lines around the character, but I don't see any of the obstacles in the debug renders.

It seems that obstacles never get added to the render buffer - I put together a test. I'm running this every frame to make sure I wasn't missing anything:

physx::PxControllerManager* controllerManager = ...
physx::PxObstacleContext* obstacleCtx = controllerManager->getObstacleContext(0);

controllerManager->setDebugRenderingFlags(physx::PxControllerDebugRenderFlag::eOBSTACLES);

physx::PxBoxObstacle obstacle;
obstacle.mPos = physx::PxExtendedVec3(0, 0, 0);
obstacle.mRot = physx::PxQuat(physx::PxIdentity);
obstacle.mHalfExtents = physx::PxVec3(1, 1, 1);

obstacleCtx->addObstacle(obstacle);

spdlog::info("obstacles: {}, lines: {}, triangles: {}", obstacleCtx->getNbObstacles(), renderBuffer.getNbLines(), renderBuffer.getNbTriangles());

Snippet of the output:

[2021-07-16 15:59:00.654] [Editor] [info] obstacles: 756, lines: 0, triangles: 0
[2021-07-16 15:59:00.660] [Editor] [info] obstacles: 757, lines: 0, triangles: 0
[2021-07-16 15:59:00.667] [Editor] [info] obstacles: 758, lines: 0, triangles: 0
[2021-07-16 15:59:00.673] [Editor] [info] obstacles: 759, lines: 0, triangles: 0
[2021-07-16 15:59:00.679] [Editor] [info] obstacles: 760, lines: 0, triangles: 0
[2021-07-16 15:59:00.685] [Editor] [info] obstacles: 761, lines: 0, triangles: 0

Is there a bug here (both in that the obstacles don't show up on the debug renderer, and I also don't seem to collide with them), or am I creating them incorrectly?

Something that could work is hack the code in outputCapsuleToStream(), so that it outputs a box instead of a capsule to the collision cache/stream. So you compute the (oriented) bounding box around the incoming capsule, and then you replicate (or call) outputBoxToStream() from outputCapsuleToStream(). We treat boxes as triangle meshes, so the slope etc should start to work then. The Gu::computeBoxAroundCapsule() function could be useful here. I can perhaps give it a try if you don't manage to make it work.

In this case I would just add all of the capsules as box colliders (but still show them as capsules to the artists in the editor ;) ), though obviously using boxes around trees (and anywhere else that we want to use capsule colliders), wouldn't be ideal since the player will collide with edges/corners that aren't visible.

for your particular case with trees you can try to use https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/Manual/CharacterControllers.html#behavior-callback in callback function you have to distinguish that the touched shape is an actual tree and if so - return PxControllerBehaviorFlag::eCCT_SLIDE.

I will give this a go, thanks for the suggestion!

From my humble experience with kinematic cct provided by physx - it's not out-of-the-box everything-included production-ready solution for AAA games. It's more like base which you can use to build what you need in your project.

Other than the capsule colliders not working it actually completely meets our needs so far! We actually moved to physx from a different physics library recently, and I'd built a CCT using the previous engine, so I'm considering maybe porting that one over as I already understand the underlying mechanism. Though I would really like to use the physx one as it is likely to be way more performant than my proposed refactor!

Thanks for the help folks!

PierreTerdiman commented 3 years ago

I noticed in the documentation that there is a capsule obstacle

Yes, although a capsule obstacle probably wouldn't help here. IIRC there is no difference internally between a regular capsule object and a capsule obstacle, so replacing one with the other won't solve your problem. But adding box obstacles around your regular capsules might.

I can see debug lines around the character, but I don't see any of the obstacles in the debug renders.

So I suspect you forgot to pass the obstacle context to the "move" function. If you do so, you should see the obstacles in the debug rendering and of course your character should collide against them.

though obviously using boxes around trees (and anywhere else that we want to use capsule colliders), wouldn't be ideal since the player will collide with edges/corners that aren't visible.

Well it should already be the case with capsules, as I suspect your render geometry for trees is also not just plain capsules. So there's already a mismatch between the collision geometry and the render geometry anyway. And it's again the same with the capsule used around the character - a rough bounding volume that doesn't match the more complicated render geometry for your character.

At the end of the day your problem is that the "slope limit" doesn't work. I suspect it will never work properly for capsules (or any rounded shape) because the "slope" of a capsule is ill-defined - it's all round everywhere. The only "slope" you can clearly define with a capsule is the orientation of its main axis, and that's basically what you solidify and pass to the system when replacing the capsule with an OBB.

Writing this though, a new approach comes to mind: change the character controller code so that it uses the capsule's orientation for the "slope limit" test, instead of e.g. the impact normal. I think that could work and it would be a neat solution to the original problem, but that requires non-trivial modifications that I'd have to investigate (I just had the idea now, I don't know if it's possible to implement the idea).

tommitytom commented 3 years ago

So I suspect you forgot to pass the obstacle context to the "move" function. If you do so, you should see the obstacles in the debug rendering and of course your character should collide against them.

You are absolutely correct on both accounts! Obstacles worked fine but the climbing issue persisted.

I'd have to investigate (I just had the idea now, I don't know if it's possible to implement the idea).

Sounds interesting - let me know what you come up with!

PierreTerdiman commented 3 years ago

Sounds interesting - let me know what you come up with!

I don't have time to look at it properly right now but here's a simpler version that uses the impact normal for static capsules. Go to CctCharacterController.cpp, find that part of the code, replace the whole ifdef / endif block with the code below.


#ifdef USE_CONTACT_NORMAL_FOR_SLOPE_TEST
                mFlags |= STF_VALIDATE_TRIANGLE_DOWN;
                mContactNormalDownPass = C.mWorldNormal;
#else
                if(touchedActor->getConcreteType() == PxConcreteType::eRIGID_STATIC)
                {
                    // 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(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);
                    }
                    else
                    {
                        PxCapsuleGeometry capsuleGeom;
                        if(touchedShape->getCapsuleGeometry(capsuleGeom))
                        {
                            mFlags |= STF_VALIDATE_TRIANGLE_DOWN;
                            mContactNormalDownPass = C.mWorldNormal;
                            mTouchedTriMax = FLT_MAX;
                        }
                    }
                }
#endif