JoltPhysics icon indicating copy to clipboard operation
JoltPhysics copied to clipboard

Virtual Character Controller Issues

Open jankrassnigg opened this issue 3 years ago • 19 comments

So I have come back to my Jolt integration and am again trying to get character controller behavior working that feels right, but everything I try doesn't work. The main goals are:

  1. Moving over small obstacles should be effortless (stair stepping) and should not lead to an aprupt change in velocity.
  2. When moving stairs downwards, the CC should stick to the ground and not lift off.
  3. I want full control over when the CC slides downwards. E.g. when it is standing on stairs, I never want it to slide the steps downwards. So it is ok if it is touching the stairs in a way that Jolt would consider it in the "sliding" state.

I'll start with problem 3) because this is related to applying gravity.

Basically I never want the CC to slide down a slope just due to gravity, but rather I want to decide that when a slope is steep, I apply a sidewards force myself. I don't like how Jolt decides that it is sliding, because it uses the contact normal for that, which in many cases is not ideal. In most cases I rather want to use the surface normal or various heuristics to determine when to slide.

Since Jolt will inevitably slide the CC sidewards if I just apply a fixed downwards force, instead I do a shape cast downwards, collect all contact points, find the closest one and then only move the CC by that distance, so that it should perfectly touch the ground without getting pushed into it. This doesn't work, it still gets pushed sidewards. I assumed that this is due to mCharacterPadding. My assumption is that when I move it to perfectly touch the ground, that's too close and Jolt will push it away to maintain the padding distance. That results in my character standing on any slope and ever so slightly sliding down (that speed is also dependent on framerate unfortunately).

So I tried to subtract the padding from the distance that I push it down, but after that failed as well, I realized that to maintain a certain distance to the ground plane, I of course have to take the angle of the ground surface into account to adjust how far I move the CC downwards. Even though I think I got the maths for that one right, I still haven't managed to get it to work, it continues sliding. Now this could be due to me still doing something wrong, or not knowing exactly how the padding comes into play, or simple float precision issues. I don't know.

Long story short, it would be nice, if there was a function on the virtual character, that would allow me to just move it into a direction until it touches something, and then just stop at the proper distance, so that I am guaranteed to get no sliding, but it also properly updates its ground state.

Talking of ground state, another thing I ran into is that when my CC is standing on the ground and I apply an upwards impulse for jumping, I move the CC several meters up high, but after Update() it still reports its own state as "OnGround" for one frame. It only changes its state to "InAir" after the next update.

To implement 2) I need the same feature as for 3).

Regarding 1) I'm simply unable to get satisfactory results. I can get the CC to step upwards, yes, but it doesn't work very well. Basically, if the step is high enough, it does what it is supposed to, but as far as I can tell, the detection for a "step" depends on the contact normal of what the CC just ran into. That means, if the step is small (or the max slope angle is large), this doesn't trigger at all, because now the CC is simply pushed upwards due to the forwards force. That means that you just bump into obstacles, get slowed down but pushed up, and then speed up again, so it is a very bumpy feeling. And whether you get the stair-stepping behavior or the brute-force push through behavior depends on the obstacle height and the max slope angle, so it is difficult to tweak.

I've copied the stair-stepping code from the sample, with the desired horizontal step and achieved horizontal step logic. However, even though the code looks like it should move the CC forward at constant speed and just make it step upwards if needed, I always get a frame where it first slows down noticeably at the obstacle, then steps upwards and continues. I'm not ruling out yet another bug on my side, but looking at the velocity printed in the Jolt CC sample, it looks like it has the same problem.

So I've taken another look at the stair stepping in the PhysX character controller and there it is very smooth. Moving up stairs looks like gliding on a plane and forward velocity isn't affected. I don't know how exactly they implemented that, but there the step-height is just part of the regular movement update. Maybe that makes it easier to integrate stair-stepping, because you don't have to mix two very different behaviors.

Any help is highly appreciated.

jankrassnigg avatar Aug 09 '22 19:08 jankrassnigg

Hello, I'm currently on holiday so I don't have the means to investigate. I'll get back to you later.

jrouwe avatar Aug 09 '22 19:08 jrouwe

Enjoy your vacation :-)

jankrassnigg avatar Aug 09 '22 20:08 jankrassnigg

Hello,

I'm starting to look into this.

Talking of ground state, another thing I ran into is that when my CC is standing on the ground and I apply an upwards impulse for jumping, I move the CC several meters up high, but after Update() it still reports its own state as "OnGround" for one frame. It only changes its state to "InAir" after the next update.

I think I fixed this on branch feature/character.

Basically I never want the CC to slide down a slope just due to gravity, but rather I want to decide that when a slope is steep, I apply a sidewards force myself. I don't like how Jolt decides that it is sliding, because it uses the contact normal for that, which in many cases is not ideal. In most cases I rather want to use the surface normal or various heuristics to determine when to slide.

I think the main issue here is that there is currently no distinction between walls and floors. If you set a horizontal velocity, you'd probably want the character to move along the wall if it is at an angle, but if it is the floor and the velocity is vertical you'd want the character to stop. I can see that that is indeed not what is happening. Perhaps an easy way of giving control over this is to add a CharacterContactSettings::mCanSlide (or perhaps a factor) so that in CharacterContactListener::OnContactAdded you can compare the character velocity and contact normal and decide if the player will slide along the contact or not. Do you think that will work for your case? Does that then remove the need to have a collision detection function? I can easily make a prototype of this on the branch above if you want to try it out.

I've copied the stair-stepping code from the sample, with the desired horizontal step and achieved horizontal step logic. However, even though the code looks like it should move the CC forward at constant speed and just make it step upwards if needed, I always get a frame where it first slows down noticeably at the obstacle, then steps upwards and continues. I'm not ruling out yet another bug on my side, but looking at the velocity printed in the Jolt CC sample, it looks like it has the same problem.

The stair stepping code inherently creates jitter in the velocity because it adds a lot of vertical displacement based on whether the stair stepping code kicked in or not. If you remove the vertical component of the printed velocity, this difference is a lot smaller (although not gone). What we do is to smooth the position a bit on the rendered mesh so that we take out the high frequency noise, but I can see if I can make this better somehow. If I read the PhysX documentation, the stair stepping code is off by default for capsule based characters and they rely only on the capsule being round for stepping up small steps (is that how you configured it?).

When moving stairs downwards, the CC should stick to the ground and not lift off.

This needs some thought as well. I think the current behavior when your run down some stairs is physically correct (you would indeed not touch the stairs if your horizontal velocity is large enough) but humans don't like falling and anticipate the stairs by changing the way they walk in a way that cannot really be approximated by a simple capsule. I'd rather not do an additional collision check at the end of an update as that's expensive, but since we've already scanned all collision around the player and converted the hit results in collision planes, maybe we can do an additional check to see if there's floor within some tolerance and then sweep down (of course this will increase the vertical velocity beyond what you would expect from gravity)? If you have separate ray casts for foot IK and pull the torso down if the legs are over extended, I think it's less noticeable that the capsule doesn't touch the ground for a short while (again a difference between the capsule and the rendered mesh, like the smoothing described above).

jrouwe avatar Aug 23 '22 20:08 jrouwe

Now I'm on vacation 😁 I'll try to reply to some of the points in the next few days, but I'll probably not going to be able to test anything in the next two weeks.

jankrassnigg avatar Aug 25 '22 18:08 jankrassnigg

No problem, enjoy your vacation :)

jrouwe avatar Aug 26 '22 07:08 jrouwe

I've done some more work on this (see #218).

I've added a CharacterContactListener::OnContactSolve that allows you to override the sliding behavior per contact (with a simple implementation in CharacterVirtualTest). This means that in the demo, the character now doesn't slide at all when standing on a slope. Still pondering a bit on the interface because it may not give enough info to do the calculation properly.

I've also added a new function StickToFloor that you can call after the Update function to project the character back onto the floor. In CharacterVirtualTest, I call this function once when the character transitions from on ground to in air. It searches for a floor max 0.5 m down and keeps the character on the floor when walking down slopes, but somehow it's not enough for the stairs (need to dig into that a bit more).

I've also looked a bit at how PhysX implements the character controller and it looks like their model does (simplified):

  1. Cast up by step offset (unless moving up or no horizontal velocity)
  2. Cast horizontal in X iterations, when hitting something it will update the velocity and cast again until the iterations run out
  3. Cast down by step offset (if applied earlier)

I think the downside of this approach is that if you have a door opening that is just large enough for the capsule to fit through, that you won't fit because you first do the step up and are then too high for the door (but maybe I'm just reading the code wrong).

My code doesn't cast up unless you call WalkStairs (it just tries to slide from A to B), and that does indeed make it more susceptible to small bumps. mCharacterPadding was added to help create smoother contact normals, increasing this should make everything less bumpy. CanWalkStairs is quite conservative in the sense that it will only return true if you're really hitting something that's too steep to move onto, as you say this may not be the best implementation. If you have any ideas let me know.

jrouwe avatar Aug 28 '22 15:08 jrouwe

Thanks a lot for investigating. Personally I find the PhysX implementation quite nice. I've used it for a couple years now, and it just felt "right" so that I never gave it a second thought. The only thing that I added was this "stick to ground" feature, by additionally making a downwards linear cast at the end and pushing the CC downwards. In PhysX that was sufficient (I didn't get the unintended slope sliding that I described in the post above). The door scenario that you describe may be possible, but I never noticed anything like that. I think what they did is basically to allow you to use the stair stepping feature or not, and if you decide for it, your CC is basically that much taller and you just have to take that into account. To be honest, that's a fair trade-off. The main thing that I didn't like about the PhysX CC was, that some of the features are general configurations that you have no control over. For example, IIRC, the stair stepping is either on or off when you create the CC, but you can't switch or tweak it later.

jankrassnigg avatar Aug 28 '22 15:08 jankrassnigg

I've merged #218. Let me know if this works better now.

jrouwe avatar Sep 11 '22 15:09 jrouwe

I'm starting to get into this topic again now that I'm back from vacation. I've tested the new stick-to-floor feature and it works like a charm :-) So that's one problem solved, I guess.

I'll take another look at the stair-stepping, but my gut-feeling is that the current solution is problematic.

I've added a CharacterContactListener::OnContactSolve that allows you to override the sliding behavior per contact

This definitely sounds powerful and I think makes sense for advanced use cases. I haven't checked it out yet, maybe it's super easy to use, but maybe it may additionally be nice to have a "SlideMode" (or factor) with which it is easy to globally disable sliding in general (for the next update step), because I would expect that most of my use cases either require all sliding or no sliding.

jankrassnigg avatar Sep 16 '22 09:09 jankrassnigg

Some random thoughts about the current stair stepping:

  1. It doesn't work well with higher framerates. I have to limit my engine to 60 FPS, otherwise it breaks down. I think this is because of this threshold: image I could change the threshold (in fact I had removed it entirely in the past) but that may introduce other artifacts.

  2. If you run into an obstacle at a shallow angle, the CC will rather slide along it, rather than step over it. See this video:

https://user-images.githubusercontent.com/6001174/190629156-ec4cbe52-c941-482e-b419-bc3d19503028.mp4

At some point it will step over it, but the end result is a not very smooth user experience.

  1. On stairs you easily run into the problem that you bump into a step, get lifted up a bit, but then end up in the sliding state and therefore slide downwards again. See this video:

https://user-images.githubusercontent.com/6001174/190630201-ffda3430-4b3d-481c-9ac2-fd75f926f8ed.mp4

I am not really sure how to deal with this. PhysX seems to differentiate between standing on an actually steep slope and sliding downwards, versus standing on the edge of a stair. I would guess that they take the triangle normal of the contact point, rather than the contact normal, to decide whether they are in the "sliding" state.

The sliding state is generally something I was wondering about. Currently this is entirely contact normal dependent, which for a capsule shape easily happens at the edges, no matter the geometry that you actually touch. This is not only problematic for stairs, but also doesn't allow to slide more easily on some surfaces (e.g. ice). Maybe it makes more sense for me to ignore Jolt's sliding state, and look at the contacts myself.

jankrassnigg avatar Sep 16 '22 11:09 jankrassnigg

I haven't checked it out yet, maybe it's super easy to use, but maybe it may additionally be nice to have a "SlideMode" (or factor) with which it is easy to globally disable sliding in general (for the next update step), because I would expect that most of my use cases either require all sliding or no sliding.

I think this is easy to implement (and exactly what I've done in the sample - note that I just found a big bug that I'm fixing). Basically you determine in OnContactAdded if you want to slide and if you don't, you just reset the velocity to zero in OnContactSolve.

I would guess that they take the triangle normal of the contact point, rather than the contact normal, to decide whether they are in the "sliding" state.

This is usually also problematic. Consider this:

image

The yellow capsule touches the black stairs. The contact normal would be something like the green arrow, the triangle normal could be one of the red arrows (depending on rounding errors). If you get the horizontal one you may consider yourself on a very steep slope.

Looking at the other points now.

jrouwe avatar Sep 16 '22 13:09 jrouwe

Yes, though if you start pushing the CC into the steps, wouldn't you get a contact point for both triangles? I thought your current code already returned the "ground contact" which had the most upward pointing normal, no?

jankrassnigg avatar Sep 16 '22 14:09 jankrassnigg

Yes you're right. I was thinking of what would happen in the rest of the algorithm when I start using the triangle normals instead of the contact normals. This would break the sliding code in SolveConstraints because you'd hit the vertical wall and stop. But if I track both normals then I could use the most upward pointing normal as the ground normal to determine if we're sliding and still use the contact normal to do the actual sliding. I'll try this.

I've investigated the other issues and I think they all boil down to the same problem.

The code you posted is wrong indeed. It is just testing the length of the desired step vs the actual step taken, but it doesn't take direction into account. So if you slide backward more than you wanted to step forward the algorithm thinks that everything went ok and doesn't use the walk stairs code.

When fixing this I ran into the next issue: when you're on a sliding slope, the velocity passed to the character is reduced:

inMovementDirection -= (dot * normal) / normal.LengthSq(); (1)

This means that:

Vec3 desired_horizontal_step = mCharacter->GetLinearVelocity() * inParams.mDeltaTime;

also becomes really small (especially in 120Hz mode) so you do only a tiny stairs step. Also since:

mCharacter->CanWalkStairs()

uses the same velocity to check if you're pushing into the stairs in the right direction, it often just doesn't call the stair walking code at all.

Finally the step down in WalkStairs doesn't take the character padding into account, so with a tiny step it often doesn't hit the floor, thinks it is in the air and then cancels the whole walk stairs.

The solution, I think, is to not do (1) in the first place (the Update function should just not slide up a slope that's too steep), but that snowballed into a whole heap of other problems that I'm currently trying to sort out.

jrouwe avatar Sep 16 '22 21:09 jrouwe

I submitted another change in #230 which fixes the bug that caused walk stairs not to kick in and the one where the step down would miss the ground. I did not succeed in removing (1) because it looks really weird if you allow the player to keep pushing against a wall (even though you don't slide up, you stick to the wall for a while before sliding down). For me it works at 120Hz now.

I've also created a branch feature/character with the changes to use the surface normal instead of the contact normal to determine if the character should slide or not. I think it's better, but I'll leave it to you to judge it.

Approaching an obstacle at a shallow angle is better now too I think, but if the angle is shallow enough you will still slide instead of step up (the delta movement towards the step is too small and the walk stairs doesn't find ground). I haven't been able to come up with a good check for this yet.

jrouwe avatar Sep 17 '22 15:09 jrouwe

Another update: After testing some more and fixing some more bugs I merged the branch (see #231). This also fixes the jitter that was present when standing in one of the funnels of Obstacle Course level / walking into a corner.

B.t.w. about ice: I think this should be implemented on the game side. The character doesn't have inertia. It is like a kinematic body that just applies whatever velocity you tell it to. If you want to simulate ice I think the game should detect the ground material and change the input velocity (slowly reduce the input velocity when the player stops moving to simulate sliding / apply extra velocity if you're on a sloped ice surface).

jrouwe avatar Sep 18 '22 10:09 jrouwe

More feedback in random order:

  1. My test level currently runs at 300+ FPS. At that speed the stair stepping doesn't work anymore. Everything else does, though, so it would be a pity if I needed to limit the update rate just for that to work.

  2. Regarding the sliding ("ice"): You are totally right, I think everything about sliding is very much the game's responsibility and there are many details to think about. I've therefore come to the conclusion that having a "sliding state" in the Jolt CC maybe is redundant to begin with? At least for me, it has put me down the path to rely on Jolt's state, when in fact I should have custom logic. Maybe the entire state could just be removed and you just return the ground contact (which is super useful) and if there is none, then the CC is "in the air" and for everything else people can look at the contact or ground normal and decide for themselves.

  3. Regarding the "stick to floor" function. Isn't this now just a "slide till contact" function (haven't looked at the impl). I'm wondering, whether this could be named more generically, because nothing should stop you from using it for other things, right? Except for the assert in there, that wants the CC to be in the air.

  4. Currently when I walk down a stair, the CC is always reported to be "in air" for a frame or so when it reaches the next step. I first call "Update" and then "StickToFloor" and then print the CC's state. Is it possible that stick-to-floor doesn't update the ground state? I would expect that it updates the full ground contact and state.

  5. With the current stair-stepping logic I can step up arbitrarily stacked boxes such as these:

    grafik

    I am currently ignoring the "sliding" state (since I want to do that custom later). If I were to not do stair stepping when sliding this shouldn't be an issue. However, I'm pretty sure then I also couldn't step up regular stairs, because once I thouch the edge it is considered sliding. This also can't be fixed with looking at the surface normal. I thought the MinStepForwards option was supposed to prevent this? I have set that to 15cm and I can reproduce stepping over walls with "steps" of any depth below that.

    Though, there are probably other ways (for me) how to prevent this, like doing a sweep downwards with a smaller capsule (or raycast) to check whether there is any ground within a certain distance below the center of the CC. Just curios what you think.

jankrassnigg avatar Sep 19 '22 07:09 jankrassnigg

My test level currently runs at 300+ FPS. At that speed the stair stepping doesn't work anymore. Everything else does, though, so it would be a pity if I needed to limit the update rate just for that to work.

I will investigate, I'm sure that can be made to work.

if there is none, then the CC is "in the air" and for everything else people can look at the contact or ground normal and decide for themselves.

I think EGroundState::Sliding is maybe a bit of a misnomer. It doesn't indicate that you're actually sliding, it indicates that you're on a slope that's too steep. It prevents you from moving up, but it doesn't actually cause you to slide down unless the game code starts applying a negative vertical velocity. I think it's an important state to return since there's actually quite a bit of calculation involved in determining this state. This is the complex case:

image

Each of the walls around the character are too steep to stand on, but the state is 'on ground' since you cannot slide down any of the slopes because they form a funnel. In this case the normal nicely averaged out to a vector pointing up, but depending on the exact situation, this may not always be the case so I'm not sure if you can only look at the normal.

Regarding the "stick to floor" function. Isn't this now just a "slide till contact" function (haven't looked at the impl). I'm wondering, whether this could be named more generically, because nothing should stop you from using it for other things, right?

No, it doesn't slide. It only does a downward cast to find the first hit and stops there. But I guess it could get a more generic name.

Is it possible that stick-to-floor doesn't update the ground state? I would expect that it updates the full ground contact and state.

It does that, but looking at the code it may be a floating point precision error. It does a downward cast to find the first hit and updates the character position. From there it does a collision check to find all contact points and determines the new ground state, but maybe that check fails because we put the character so that it is exactly touching the floor (i.e. a small epsilon too high for the collision check to find something). I will take a look.

With the current stair-stepping logic I can step up arbitrarily stacked boxes such as these:

I'm guessing you mean that you don't want the character to step up boxes like this (because the horizontal surface is not deep enough)? I at first thought this would be due to the new code that uses the surface normal instead of the contact normal:

image

But I couldn't reproduce the issue. Steps with a depth of up to around 10 cm do not cause the character to step up in my test. Are you seeing something else? And if so what are the dimensions of your character / the steps / etc.?

I am currently ignoring the "sliding" state (since I want to do that custom later). If I were to not do stair stepping when sliding this shouldn't be an issue. However, I'm pretty sure then I also couldn't step up regular stairs, because once I thouch the edge it is considered sliding. This also can't be fixed with looking at the surface normal. I thought the MinStepForwards option was supposed to prevent this? I have set that to 15cm and I can reproduce stepping over walls with "steps" of any depth below that.

I'm having trouble parsing this paragraph, so I'm not sure what you're asking. Let me try to explain how it works:

image

We start at 1 then first we will test at 2 inStepForward and find that the slope is too steep. We then test again at 3 inStepForwardTest and find a nice vertical normal. The actual step that we will take forward is then step 2 (which will cause the character to be marked as sliding, which is not nice). The reasoning behind this was that if we were to move to 3 that we would suddenly get a lot of horizontal movement, but I'm thinking that maybe that whole inStepForwardTest thing should removed and we should just always pass a minimal distance to inStepForward and accept a horizontal speedup for 1 frame (this will probably also fix the 300Hz issue, it will fix that you end up in a sliding state and it will save a lot of collision detection time).

jrouwe avatar Sep 19 '22 19:09 jrouwe

Regarding the sliding, yes I am aware that the CC isn't sliding unless I actually push it down. Though I definitely wasn't aware of the fact that Jolt makes sure to switch to "on ground" in such special cases. That's very good to know.

However, now that raises another question: We already talked about doing "ice" and other stuff on a higher level. Considering that there is this extra logic to deal with funnels, would you advise that to properly support that, one should dynamically adjust the max slope angle depending on the type of surface beneath the CC? And would that be a problem?

Maybe better state names would be "OnFlatGround" and "OnSteepGround"? As always, naming things is the hardest part :D

No, it doesn't slide. It only does a downward cast to find the first hit and stops there.

Yep, I shouldn't have used the word "slide" here. I meant "cast". Anyway, my point is, it could be used for other things than stick-to-floor, if somebody just wants to move a CC exactly to the point of the first contact along the given direction.

I'm guessing you mean that you don't want the character to step up boxes like this.

Exactly.

And if so what are the dimensions of your character / the steps / etc.?

From the top of my head the capsule has a radius of 0.25 and an additional length of 1.0. The steps are 0.1 units large and the offset of the 3 steps was around 0.02 to 0.05 each. The minStepForwards is 0.15. The simulation was locked to 60 FPS. Now that I think about it, it's possible that I allowed the CC to step over 0.3 high obstacles, and the three boxes combined where still <= that ... need to look at that again tomorrow.

I'm having trouble parsing this paragraph

Sorry, re-reading it again, it's indeed hard to decipher :D Let me try again.

I am currently always executing the stair-stepping function, even if Jolt tells me that the CC is in the "sliding" state. With such geometry that means the CC barely touches the first step but also touches solid ground and is then pushed up onto the first step by the stair-stepping. Now it floats above ground and the only contact it has is very much at the side. Jolt should then probably tell me that the CC is now "sliding". Since I execute the stair-stepping anyway, pushing further into the wall means that I can step up again and again and thus get all up the wall.

My assumption is, that if I do not execute the stair-stepping when the CC is in the "sliding" state, this particular issue should go away.

On the other hand, the reason why I currently do execute the stair-stepping, even when the CC is "sliding", is because in the past I had problems stepping up regular stairs, otherwise, Maybe after your recent changes that's not the case anymore, I will have to try that out again.

maybe that whole inStepForwardTest thing should removed and we should just always pass a minimal distance to inStepForward and accept a horizontal speedup

Maybe. But what would the minimum distance be? It would probably depend on the capsule's radius (to get to a point where it's not considered sliding anymore) and that may be quite a large distance in just one frame.

jankrassnigg avatar Sep 19 '22 21:09 jankrassnigg

Considering that there is this extra logic to deal with funnels, would you advise that to properly support that, one should dynamically adjust the max slope angle depending on the type of surface beneath the CC? And would that be a problem?

I hadn't thought of that, but there's no caching of anything related to the max slope angle, so you can change it before every update if you want. It sounds like a sensible thing to do if you want different materials to have different behaviors (obviously you'll always be 1 frame too late in reacting to the new floor).

Maybe better state names would be "OnFlatGround" and "OnSteepGround"? As always, naming things is the hardest part :D

OnSteepGround -> yes, OnFlatGround -> most of the time you're not on flat ground at all when this value is returned. I think I'll go for OnGround and OnSteepGround.

Anyway, my point is, it could be used for other things than stick-to-floor, if somebody just wants to move a CC exactly to the point of the first contact along the given direction.

Agreed. I'll make it more generic.

My assumption is, that if I do not execute the stair-stepping when the CC is in the "sliding" state, this particular issue should go away.

I think so too. If there are still issues with the walk stairs code then let me know and I'll see what I can do about it.

But what would the minimum distance be? It would probably depend on the capsule's radius (to get to a point where it's not considered sliding anymore) and that may be quite a large distance in just one frame.

I think the minimum distance is something that the game should decide. It's probably in the order of 5-10 cm in order get a contact normal that's not too horizontal. It's a tradeoff between the player warping forward during the step up for 1 frame and how easy you can step up.

jrouwe avatar Sep 20 '22 19:09 jrouwe

I think I'll go for OnGround and OnSteepGround.

This is done in #232.

Is it possible that stick-to-floor doesn't update the ground state? I would expect that it updates the full ground contact and state.

This was indeed a float precision error. Fixed in #232.

My test level currently runs at 300+ FPS. At that speed the stair stepping doesn't work anymore. Everything else does, though, so it would be a pity if I needed to limit the update rate just for that to work.

I've tried to reproduce this, but couldn't get it to fail. My guess is that you can fix it on your side by clamping inStepForward to a minimum distance in WalkStairs at these high frequencies.

Anyway, my point is, it could be used for other things than stick-to-floor, if somebody just wants to move a CC exactly to the point of the first contact along the given direction.

Tried generalizing this, but StickToFloor needs to abort and return to the previous position when no floor is found, so I can't make this a generic 'cast to position' function. I'll provide another function later for moving the character.

maybe that whole inStepForwardTest thing should removed and we should just always pass a minimal distance to inStepForward and accept a horizontal speedup

I tried this, but it didn't look better, so I reverted it.

jrouwe avatar Sep 23 '22 14:09 jrouwe

Just a heads up, I just ran into this assert while testing:

image

While traversing an obstacle on the ground.

My stair stepping logic is copied from your sample, so CanWalkStairs returned true etc. Maybe a floating point issue.

"contacts" is empty. All the other inputs look normal. Simulation was locked to 60 Hz.

jankrassnigg avatar Sep 26 '22 07:09 jankrassnigg

Regarding stepping up walls, here is a video of a case that's not working as expected for me at the moment (without custom work-arounds):

https://user-images.githubusercontent.com/6001174/192569903-c63d775b-4b65-4488-baef-4f1097b78c9c.mp4

Capsule radius is 0.25, height 1.0, cMinStepForward = 0.3 (was 0.15 but tried extra large now), stepUp = 0.3. The wall has a thickness of 0.25, so the part that the CC can stand on is roughly 0.05 deep.

I only execute the stair stepping code when the ground state is OnGround. I thought that the fairly large cMinStepForward would actually prevent the stair stepping from being executed in such cases? Different values didn't seem to have a different effect.

jankrassnigg avatar Sep 27 '22 15:09 jankrassnigg

Regarding the high framerate issue, here is a video showing the stair stepping at 60 FPS:

https://user-images.githubusercontent.com/6001174/192575539-474f42d1-236c-4bd4-857d-e91326001811.mp4

And here it is at 200 FPS:

https://user-images.githubusercontent.com/6001174/192575613-80c67c3f-eed1-4b6e-8cce-da9eb5dfdc40.mp4

The logic for the stair stepping is copied from your sample, though I removed the hard-coded float threshold (also tried it with the threshold, same problem).

In the second video you actually see two problems.

At first the contact normal is UP and therefore the ground state is OnGround and therefore the code detects that it didn't get far enough, Jolt reports that it CAN step up the stairs (see debug text top left) and executes the stair stepping, but then nothing happens. Not sure why that is, but apparently the smaller time-step somehow makes it cancel the stair stepping.

In the second case, the reported contact normal is not the UP surface's normal, but the wall's side. Thus the ground state is "Steep" and therefore the entire stair stepping isn't even attempted to get executed. I assume that since the time-step is shorter, the CC moves less into the geometry and thus only finds one of the contacts, maybe?

jankrassnigg avatar Sep 27 '22 16:09 jankrassnigg

Just a heads up, I just ran into this assert while testing

Interesting assert. It means that the walk stairs code, while doing a sweep down found a collision, but then when we find all collisions at the sweep collision location it doesn't find any collision anymore. What's the value for mPredictiveContactDistance that you have (the default is 10 cm, which should be well above any floating point error that you can get)?

Not sure why that is, but apparently the smaller time-step somehow makes it cancel the stair stepping.

Did you make the stair stepping execute a minimal horizontal displacement? I.e. in the sample change:

Vec3 step_forward = step_forward_normalized * (desired_horizontal_step_len - achieved_horizontal_step_len);

to

Vec3 step_forward = step_forward_normalized * max(0.01f, desired_horizontal_step_len - achieved_horizontal_step_len);

where 0.01f is a minimal forward movement to do in 1 frame (value is a guess, it could be that you need a couple of cm).

Thus the ground state is "Steep" and therefore the entire stair stepping isn't even attempted to get executed.

In the sample, I think that should trigger stair stepping. See CharacterVirtualTest::PrePhysicsUpdate, first it checks if the travelled distance is too small (which should be the case here) and then it calls CharacterVirtual::CanWalkStairs which checks that the slope is 'steep' (which should also be the case). Are you sure your logic is the same?

Maybe you can export your test level geometry with code similar to SamplesApp::TakeSnapshot so that I can play around with it?

jrouwe avatar Sep 27 '22 21:09 jrouwe

B.t.w. I was trying to compile and run ezEngine myself to see what's going on (dev branch) but when I run GenerateWin64vs2022.bat I get:

'C:\Users\jrouw\Documents\Code\ezEngine\Data\Content\AssetCache\LastSubmoduleUpdate.txt'.
At C:\Users\jrouw\Documents\Code\ezEngine\Generate.ps1:36 char:9
+         Out-File -FilePath $LAST_UPDATE_FILE -InputObject $CURRENT_CO ...
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (:) [Out-File], DirectoryNotFoundException
    + FullyQualifiedErrorId : FileOpenFailure,Microsoft.PowerShell.Commands.OutFileCommand```

If I run the CMake UI and hit Configure I get:

```CMake Error at CMakeLists.txt:36 (project):
  No CMAKE_CSharp_COMPILER could be found.```

(I have VS2022 with C# installed)

jrouwe avatar Sep 28 '22 20:09 jrouwe

Dann, I became aware of that issue a few days ago but haven't fixed it yet. Will do that tomorrow and let you know what to do to test things. Though that C# compiler thing is new to me. Maybe you tried to generate that solution for another VS version where you don't have C# installed?

jankrassnigg avatar Sep 28 '22 21:09 jankrassnigg

I fixed the Powershell script. If you try again, please check out the branch "user/jk/jolt", it has my recent character controller experiments in there. If you still see the C# thing, I would guess that you haven't installed everything for C# that CMake expects. Unfortunately that's often a guessing game of installing more MSVC components until it starts working.

I've also submitted a top-level Readme to that branch with some instructions how to get started.

jankrassnigg avatar Sep 29 '22 16:09 jankrassnigg

Thanks, it works now. The reason it didn't find a C# compiler was that I had C# for UWP installed and not C# for desktop. I'll take a look.

jrouwe avatar Sep 29 '22 18:09 jrouwe

I can repro most of the issues.

Sometimes Jolt asserts that it didn't find the expected contact.

I know what this is caused by now but I don't have a good solution yet. Basically the character hits the side of the stairs and gets a near horizontal normal, at the end of CharacterVirtual::GetFirstContactForSweep the returned fraction is corrected for this normal. Normally this is a conservative estimate of how far you can walk, but in this case causes the sweep down to end up above the stairs and CharacterVirtual::MoveToContact to not find a contact and assert.

Stair-stepping doesn't always work in general.

This I didn't see (unless you mean that it tends to slide along steps when you approach them at an angle).

Stair-stepping rarely works at high framerates (150+).

I fixed this with the following:

image

The 1.0e-4 tolerance is important, if you don't have this you can see that it tries to do a stair walk (flickering text Not far enough) even on a flat floor. Achieved and desired step length are so close that a little roundoff can cause the condition to become true.

The ezMath::Max change is what I suggested above and indeed makes the step distance large enough so that the walk stairs succeeds at 200+ fps.

Lastly, I had to change this:

image

By calculating the velocity based on old and new position, you effectively get a large up velocity if you walk stairs / a small up velocity when you walk over a small bump. Both of these cause the character to do a jump and go to the in air state. Note that m_fVelocityUp may not be needed anymore since you can also just use the characters velocity.

With foot-check disabled, one can step over the steep wall.

I can repro this but I haven't had time to investigate yet.

Other things I noticed:

  • GetContactVelocityAndPushAway: You should use CharacterVirtual::GetGroundVelocity, using GetPointVelocity for the ground velocity introduces an error that will cause the character to slide off a rotating surface (take a look at CharacterVirtual::UpdateSupportingContact, a character on a rotating platform will have a point velocity that is perpendicular to the vector to the rotation point but you should use a velocity that approximates the arc that you should be making).
  • GetContactVelocityAndPushAway: Applying a downward force should happen automatically in CharacterVirtual::HandleContact, I think you're doing it twice now.
  • You should add Jolt.natvis to your Jolt project for a better debugging experience
  • This should probably be +=: image
  • This replaces the ground velocity: image You probably want to use vVelocityToApply.z = ezMath::Max(vVelocityToApply.z, m_fVelocityUp)

jrouwe avatar Sep 29 '22 21:09 jrouwe

That's a very thorough investigation of my messy code :D Thanks for all the feedback, I'm getting closer and closer to what I want.

This I didn't see (unless you mean that it tends to slide along steps when you approach them at an angle).

Yes, that's what I meant. Though sometimes a 30° angle seems to be enough already.

I fixed this with the following:

Ah right, I forgot about that after you mentioned having improved high framerate situations. Sorry about that. Though I think you should include that in your sample code, with a comment about high framerates, so that others don't run into the same issue.

CharacterVirtual::GetGroundVelocity

I hadn't thought about testing rotating platforms yet, but you are correct that I get an error and at some point fall off the platform, so I've changed the code to use CharacterVirtual::GetGroundVelocity(). However, I still get such an error and fall off. Maybe the error is smaller, hard to say, but the problem persists. It is relatively small, though, so I don't see this as critical.

You should add Jolt.natvis to your Jolt project for a better debugging experience

Done :)

Lastly, I had to change this [everything regarding up velocity]

So this is where it gets tricky. The problem is, if I always apply a downwards force, even while standing on solid ground, I get unwanted behavior. Specifically, I get way more sliding downwards. The fact that I use a capsule shape means that there are many situations where the CC might barely touch something at it's sides (e.g. when walking up stairs). Then applying a downward force will make it slide downwards, which is not what I want. It also means that Jolt will report that it is in the OnSteepGround state, which may be technically true for the contact between the shapes, but not true from a player's perspective. That is why I use my additional filter to detect ground contacts within the step-height to determine whether I also think that this is steep ground or not.

So in practice I don't apply a downwards force, if I consider the CC to be standing on solid ground.

For instance here my check detects ground within the small capsule:

grafik

Consequently I apply no downwards force, meaning I can walk along even the thin geometry without slipping down. Only once there is no contact within that capsule, I do so, and make the CC slip down, like here:

grafik

If I take your change to retrieve the linear velocity from the CC (without any additional checks), the CC starts sliding down in too many cases.

Another issue is, that when you jump upwards and hit a ceiling, you don't immediately fall down again, but you are stuck at the ceiling until gravity has removed all the upwards force. This was the original reason for me checking the before and after Z position. When I saw your change I thought that it makes sense, because I expected the CC to actually return the linear velocity that it ACHIEVED (excluding the jump from the stair stepping). But it doesn't look like that's the case. It looks like it just returns the same value that I put in before updating the CC. Is it supposed to be that way?

So the fact that I usually don't apply a downwards force is also the reason why I manually apply a downwards force on dynamic objects. It is correct that with your change I would apply this twice.

And yes, I currently zero out any upwards velocity from the ground. My thinking is simply that if I stand on a platform that moves up or down, I will always get pushed up properly, and moving down is taken care of by the stick-to-ground feature. As long as the platform doesn't move too fast at least. IIRC when I do apply the up velocity from dynamic objects that the CC stands on, I had unfortunate results, because for example jumping onto a rolling object could suddenly transfer a large up or down velocity to the CC.

Finally, an unexpected thing I noticed during testing:

grafik

The turquoise cone is the ground contact that Jolt reports. I'm not sure whether it could be a problem that it is so high up? Maybe not, just wanted to make you aware of it.

jankrassnigg avatar Sep 30 '22 14:09 jankrassnigg