Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3D KinematicBody slides with move_and_collide #18433

Closed
Tracked by #45333
bmolyneaux opened this issue Apr 25, 2018 · 30 comments · Fixed by #50063
Closed
Tracked by #45333

3D KinematicBody slides with move_and_collide #18433

bmolyneaux opened this issue Apr 25, 2018 · 30 comments · Fixed by #50063

Comments

@bmolyneaux
Copy link
Contributor

Godot version:
3.0.2

OS/device including version:
Windows 10

Issue description:
KinematicBody slides slowly along a slope when applying constant downwards movement using move_and_collide(). Expected it to come to a complete stop after first collision with the slope.

This issue was not reproducible with KinematicBody2D. Safe Margin is set to 0.001.

Steps to reproduce:

  • Create KinematicBody with a CollisionShape (either BoxShape or CapsuleShape).
  • Add a script to the KinematicBody:
extends KinematicBody

func _physics_process(delta):
	move_and_collide(Vector3(0, -1, 0))

Create a slope using StaticBody with a CollisionShape.

Minimal reproduction project:
move_and_collide_sliding.zip

@xphere
Copy link

xphere commented Jun 11, 2018

This is also happening in 2D, the kinematic body slides down the slope when using move_and_collide. It can be reduced to a minimum setting to almost zero the safe margin in the kinematic body. But it's sliding anyway, only slower.

I made a minimum project with this behaviour, just make sure Debug/Visible Collision Shapes is enabled.

@Enoh32
Copy link

Enoh32 commented Jul 18, 2018

I've run into this problem while trying to create a character controller that also handles stair stepping. Using move_and_collide() as a sort of trace and down-stepper. Only now they slide down slopes at speeds determined through the safe margin :(

@kraptor
Copy link

kraptor commented Jul 24, 2018

Same here. Any workarounds? I'm using 3.0.5

@xphere
Copy link

xphere commented Jul 24, 2018

Setting the kinematic body safe margin to a very very small value should mitigate the sliding.

@and3rson
Copy link
Contributor

and3rson commented Jan 25, 2019

Sorry for bumping an old issue, but I can still reproduce this even with margin set to 0.001 (lowest value which the editor allows.)

@wareya
Copy link
Contributor

wareya commented Jun 4, 2019

Making the "move the object outside of the solid world" step optional might go a long way towards making this less of a problem. The "pop outside of anything you're touching" step should only happen once a frame, but people using move_and_collide might want to use it multiple times per frame.

If you're trying to write your own movement solver (which is absolutely imperative for some genres!) then you're going to be using move_and_collide until you're nearly touching things multiple times a frame, inside the safety margin, but that doesn't mean you want to pop out of them yet.

Popping out of the world can also sometimes be done better by the movement solver, at least in simple cases like "hey, I'm standing on a slope, I only want to pop out of it in a straight vertical line".

@jitspoe
Copy link
Contributor

jitspoe commented Jul 10, 2019

It would be nice if, instead of moving to a point where the collision needs to push the KinematicBody out, move_and_collide() moved all the way up to, and stopped at, the point where a collision would happen. Like, move_and_collide(), not move_and_penetrate_then_push_away_from_the_normal(). :)

@raincomplex
Copy link

Workaround:

var pos = transform.origin
var col = move_and_collide(movevec * delta)
if col:
    transform.origin = pos + movevec.normalized() * col.get_travel()

IMHO, move_and_collide() should be doing it this way already (and is what I would expect from the description in the docs). Otherwise why also have move_and_slide()? (;

@jitspoe
Copy link
Contributor

jitspoe commented Dec 4, 2019

I couldn't get this workaround to work. The travel is a vector3, so multiplying it by the move direction just got crazy results and teleported my character into walls and such. I tried adding the pre-move transform and travel together, but that just had the same result as not using the workaround (still had the slow slide down hills). I also tried to get the length of the travel and multiply that by the move direction, but that ended up making the character fall through the ground.

Looking at the source, it seems the travel combines the movement and recover motion, so I don't think using this value can result in anything much different from the initial behavior.

I tried messing around with the remainder, since that doesn't take the depenetration into account, but that resulted in the character slowly sinking into the ground.

@jitspoe
Copy link
Contributor

jitspoe commented Dec 4, 2019

After a few hours digging around in the physics code, I eventually decided to do an ugly hack:

If the horizontal velocity is 0, and the ground touched has a walkable slope, I set the horizontal component of the position to the position prior to moving.

This specifically fixes the case of not moving while on a slope causing the character to slide down, but does not fix the fundamental issue of the margin nudging things around to unexpected locations (instead of the move_and_collide() just stopping at the desired collision point).

The bulllet code is broken into 3 phases (see SpaceBullet::test_body_motion()):

  1. depenetration using margin
  2. sweep without margin
  3. depenetration (again) with margin -- collision info is returned from this, which is why it's sometimes wonky and not the actual surface normal.

I tried disabling both of the depenetrations and actually got much better results for the most part. Unexpected sliding and nudging were gone, and the normals seemed to be correct (most notably on non-uniformly scaled collision, which can get really wonky, especially with small margins). Unfortunately, this resulted in the character sticking frequently when moving parallel to surfaces (such as walking on the ground). Fixing this would likely require nudging the kinematic body slightly, bringing us back to square 1. I wonder how other engines handle this.

@wareya
Copy link
Contributor

wareya commented Dec 4, 2019

Other engines are basically just very careful about exactly when they do depenetration, and sometimes "how" as well. move_and_collide would make a lot more sense if there were some way to control when and how depenetration happens, like only doing it once per frame even if you call move_and_collide multiple times per frame, or making it try to go straight up/down before falling back to using collision normals.

@Bropocalypse
Copy link

Thanks @raincomplex for the solution! It was enough to get me the rest of the way there, which I accomplished by rounding off the starting position and movement vector to a fairly precise grid via Vector2.snapped(Vector2(0.000001, 0.000001)).

@winteraa
Copy link

winteraa commented Feb 4, 2020

it seems to me that the issue is SpaceBullet::recover_from_penetration
instead of moving the collider back the path it came from, it calculates the recover motion vector with the normal of the collision.
see here:

r_delta_recover_movement += result.m_normalOnBInWorld * (result.m_distance * -1 * p_recover_movement_scale);

With that, when hitting a collider with an angle, when the recovering happens it basically is reflected from the collision object, moving back along the collision normal.
This happens every frame, hence the object appears to be sliding along the slope.

@wareya
Copy link
Contributor

wareya commented Feb 4, 2020

recover_from_penetration is used not just after tracing but also before tracing, intended purpose being pushing the object outside of anything it's already overlapping (depenetration). The code you're pointing out is correct in that context (depenetration).

@jitspoe
Copy link
Contributor

jitspoe commented Feb 5, 2020

I tried rewriting the test_body_motion function to not use the margin or recover from penetration and have it PRETTY CLOSE to working the way I would expect it to, but there are still a few oddities/edge cases that I need to work around. This is what I have so far:


	btTransform body_transform;
	G_TO_B(p_from, body_transform);
	UNSCALE_BT_BASIS(body_transform);
	btTransform start_transform(body_transform);
	btVector3 motion;
	G_TO_B(p_motion, motion);
	bool has_collision = false;
	bool needs_iteration = true;
	bool needs_unstuck = false;
	int iterations_left = 3; // Max of 3 iterations
	real_t unstuck_margin = 0.001; // TODO: use margin settings?
	btVector3 unstuck_offset(0.0, 0.0, 0.0);
	real_t allowed_penetration = 0.0; // dynamicsWorld->getDispatchInfo().m_allowedCcdPenetration

next_iteration:
	// Do a sweep.  If something hits without movement, back out along normal and try again.
	while (needs_iteration) {
		needs_iteration = false;
		const int shape_count(p_body->get_shape_count());

		for (int shIndex = 0; shIndex < shape_count; ++shIndex) {
			if (p_body->is_shape_disabled(shIndex)) {
				continue;
			}

			if (!p_body->get_bt_shape(shIndex)->isConvex()) {
				// Skip no convex shape
				continue;
			}

			if (p_exclude_raycast_shapes && p_body->get_bt_shape(shIndex)->getShapeType() == CUSTOM_CONVEX_SHAPE_TYPE) {
				// Skip rayshape in order to implement custom separation process
				continue;
			}

			btConvexShape *convex_shape_test(static_cast<btConvexShape *>(p_body->get_bt_shape(shIndex)));

			if (needs_unstuck) {
				// I thought maybe if we moved OUT of collision, it wouldn't have an immediate hit, but apparently it does, so we'll have to unsafely back out :|
#if 1
				btTransform shape_unstuck_from = start_transform * p_body->get_kinematic_utilities()->shapes[shIndex].transform;
				btTransform shape_unstuck_to(shape_unstuck_from);
				shape_unstuck_to.getOrigin() += unstuck_offset;
				GodotKinClosestConvexResultCallback btResult(shape_unstuck_from.getOrigin(), shape_unstuck_to.getOrigin(), p_body, p_infinite_inertia);
				btResult.m_collisionFilterGroup = p_body->get_collision_layer();
				btResult.m_collisionFilterMask = p_body->get_collision_mask();
				dynamicsWorld->convexSweepTest(convex_shape_test, shape_unstuck_from, shape_unstuck_to, btResult, allowed_penetration);
				body_transform.getOrigin() = start_transform.getOrigin() + btResult.m_closestHitFraction * unstuck_offset;
#else
				//shape_world_from.getOrigin() += unstuck_offset;
				body_transform.getOrigin() += unstuck_offset;
#endif
			}

			btTransform shape_world_from = body_transform * p_body->get_kinematic_utilities()->shapes[shIndex].transform;
			btTransform shape_world_to(shape_world_from);
			shape_world_to.getOrigin() += motion;
			GodotKinClosestConvexResultCallback btResult(shape_world_from.getOrigin(), shape_world_to.getOrigin(), p_body, p_infinite_inertia);
			btResult.m_collisionFilterGroup = p_body->get_collision_layer();
			btResult.m_collisionFilterMask = p_body->get_collision_mask();

			dynamicsWorld->convexSweepTest(convex_shape_test, shape_world_from, shape_world_to, btResult, allowed_penetration);

			if (btResult.hasHit()) {
				// If we get stuck immediately moving close to parallel to a surface, back up a little bit and try again.
				if (btResult.m_closestHitFraction == 0.0 && iterations_left > 0 && motion.normalized().dot(btResult.m_hitNormalWorld) > -0.01) {
					// Stuck immediately.  Try to move out a bit.
					--iterations_left;
					needs_iteration = true;
					needs_unstuck = true;
					unstuck_offset = btResult.m_hitNormalWorld * unstuck_margin;
					goto next_iteration;
				} else {
					/// Since for each sweep test I fix the motion of new shapes in base the recover result,
					/// if another shape will hit something it means that has a deepest penetration respect the previous shape
					motion *= btResult.m_closestHitFraction;
					/// jitspoe - fix case where collision happens but we don't get any results returned.
					has_collision = true;

					if (r_result) {
						const btRigidBody *btRigid = static_cast<const btRigidBody *>(btResult.m_hitCollisionObject);
						CollisionObjectBullet *collisionObject = static_cast<CollisionObjectBullet *>(btRigid->getUserPointer());

						B_TO_G(motion, r_result->remainder); // is the remaining movements
						r_result->remainder = p_motion - r_result->remainder;

						B_TO_G(btResult.m_hitPointWorld, r_result->collision_point);
						B_TO_G(btResult.m_hitNormalWorld, r_result->collision_normal);
						//B_TO_G(btRigid->getVelocityInLocalPoint(r_recover_result.pointWorld - btRigid->getWorldTransform().getOrigin()), r_result->collider_velocity); // It calculates velocity at point and assign it using special function Bullet_to_Godot
						r_result->collider = collisionObject->get_self();
						r_result->collider_id = collisionObject->get_instance_id();
						//r_result->collider_shape = r_recover_result.other_compound_shape_index;
						//r_result->collision_local_shape = r_recover_result.local_shape_most_recovered;
					}
				}
			}
			// TODO: Move back by unstuck offset.
		}

		body_transform.getOrigin() += motion;
//next_iteration:
	}

	if (needs_unstuck) { // Move back by unstuck amount to stop stuff from floating.
		btTransform correct_unstuck_to(body_transform);
		correct_unstuck_to.getOrigin() -= unstuck_offset;
		GodotKinClosestConvexResultCallback btResult(body_transform.getOrigin(), correct_unstuck_to.getOrigin(), p_body, p_infinite_inertia);
		btResult.m_collisionFilterGroup = p_body->get_collision_layer();
		btResult.m_collisionFilterMask = p_body->get_collision_mask();
		int shIndex = 0;// TODO: Handle multiple shapes in one object.
		btConvexShape *convex_shape_test(static_cast<btConvexShape *>(p_body->get_bt_shape(shIndex)));
		dynamicsWorld->convexSweepTest(convex_shape_test, body_transform, correct_unstuck_to, btResult, allowed_penetration);
		body_transform.getOrigin() -= btResult.m_closestHitFraction * unstuck_offset;
	}

	if (r_result) {

		if (!has_collision) {
			r_result->remainder = Vector3();
		}

		motion = body_transform.getOrigin() - start_transform.getOrigin();
		B_TO_G(motion, r_result->motion);
	}

	return has_collision;
}

The old code was kind of sketchy in that it allowed the movement to penetrate a set amount during the sweep, then it recovered from the penetration, rather than simply sweeping and colliding:

dynamicsWorld->convexSweepTest(convex_shape_test, shape_world_from, shape_world_to, btResult, dynamicsWorld->getDispatchInfo().m_allowedCcdPenetration);

m_allowedCcdPenetration was 0.04, the default margin value. I was actually using a different margin value than that. Not sure if that makes things better or worse.

Things I've noticed with my changes: Issues with bad normals are MOSTLY fixed. For whatever reason, I still get some bad normal when moving quickly. Not sure why that would make any difference, but... shrug. Also some precision issues that seem worse than I would expect (ex: start colliding, back up, then move back down more than I moved up, but don't collide). Bullet doesn't always report a collision if you start inside of something. Sometimes catch the edge of two objects that are flush next to each other. This was something I was hoping to fix, but instead made worse. :|

Easiest way to repro bad normals is to take a box with collision and scale it a bunch non-uniformly (like make it super long). With the old logic, you'll get normals as though you're running on bumpy ground. With this code it's closer to what it should be.

Still need to iterate on this a bit more. Might be a while before I get back to it. Feel free to take my WIP code and tinker with it.

@wareya
Copy link
Contributor

wareya commented Feb 5, 2020

Bullet's convexSweepTest (or more specifically the btConvexCast::calcTimeOfImpact functions that it eventually depends on, see here) works by having distance support functions, which return a conservative approximation of distance, and moving closer to the target body until the distance support function is smaller than a certain value (0.0001 by default in 32-bit bullet) or it loops too many iterations (32 in 32-bit bullet).

So you can't rely on it to give particularly precise, or stable, results, especially with wildly different speeds. Also, cylinders and meshes are more likely to give bad normals than other shapes in bullet.

@jitspoe
Copy link
Contributor

jitspoe commented Feb 6, 2020

Oof. What's a guy have to do to get a nice, clean sweep of a shape that just does some exact math for a convex intersection point? I'm mostly doing box to box collision tests. Is that too much to ask for? I don't want these mushy physics for my platformer. :(

@jtaart
Copy link

jtaart commented Feb 7, 2020

Just tried it in GodotPhysics and Bullet. They both seem to do the slide thing. Just wanted to point that out. (edited)

@wareya
Copy link
Contributor

wareya commented Feb 7, 2020

I was responding to consistency concerns in jitspoe's post.

@akien-mga
Copy link
Member

CC @madmiraal

@KoBeWi
Copy link
Member

KoBeWi commented Dec 25, 2020

Still valid in 3.2.4 beta4

@madmiraal
Copy link
Contributor

This is fixed with #35945 and its 3.2 version #37498.

@pouleyKetchoupp
Copy link
Contributor

Can be still reproduced in 3.2.4 beta 5, but occurs only when using Bullet Physics (default settings). Switching to Godot Physics 3D fixes the issue.

I've opened #45004 for the 2D version of this bug from #18433 (comment) to track it separately.

@pouleyKetchoupp pouleyKetchoupp changed the title 3D KinematicBody slides with move_and_collide [Bullet] 3D KinematicBody slides with move_and_collide Jan 8, 2021
@snougo
Copy link

snougo commented Jan 9, 2021

Can be still reproduced in 3.2.4 beta 5, but occurs only when using Bullet Physics (default settings). Switching to Godot Physics 3D fixes the issue.

I've opened #45004 for the 2D version of this bug from #18433 (comment) to track it separately.

me too, but my case is whatever physics engine you chosed that it still slides on MacOS

@pouleyKetchoupp
Copy link
Contributor

@snougo Could you please share a minimal project for your case?

@snougo
Copy link

snougo commented Jan 12, 2021

@pouleyKetchoupp
bug report.zip

@pouleyKetchoupp pouleyKetchoupp changed the title [Bullet] 3D KinematicBody slides with move_and_collide 3D KinematicBody slides with move_and_collide Jan 12, 2021
@pouleyKetchoupp
Copy link
Contributor

@snougo Thanks!

Switching back to non-Bullet specific issue since it can be reproduced in Godot Physics with the MRP from #18433 (comment).

@manglemix
Copy link

manglemix commented Feb 9, 2021

Workaround:

var pos = transform.origin
var col = move_and_collide(movevec * delta)
if col:
    transform.origin = pos + movevec.normalized() * col.get_travel()

IMHO, move_and_collide() should be doing it this way already (and is what I would expect from the description in the docs). Otherwise why also have move_and_slide()? (;

Not sure if my input is still helpful but a proper way to avoid sliding using gdscript would be

var col := move_and_collide(movement_vector * delta)

if col:
    global_transform.origin -= col.travel.slide(movement_vector.normalized())

@jitspoe
Copy link
Contributor

jitspoe commented Apr 9, 2021

Workaround:

var pos = transform.origin
var col = move_and_collide(movevec * delta)
if col:
    transform.origin = pos + movevec.normalized() * col.get_travel()

IMHO, move_and_collide() should be doing it this way already (and is what I would expect from the description in the docs). Otherwise why also have move_and_slide()? (;

Not sure if my input is still helpful but a proper way to avoid sliding using gdscript would be

var col := move_and_collide(movement_vector * delta)

if col:
    global_transform.origin -= col.travel.slide(movement_vector.normalized())

Maybe I'm just tired, but this doesn't compute.
image

If we have movement straight down into a slope (black arrow), we'll get pushed out a bit along the normal because of the margins (red arrow). What you're suggesting is that we move back the offset we traveled before colliding, projected along the vector of the direction we traveled (cyan arrow)?

@manglemix
Copy link

Workaround:

var pos = transform.origin
var col = move_and_collide(movevec * delta)
if col:
    transform.origin = pos + movevec.normalized() * col.get_travel()

IMHO, move_and_collide() should be doing it this way already (and is what I would expect from the description in the docs). Otherwise why also have move_and_slide()? (;

Not sure if my input is still helpful but a proper way to avoid sliding using gdscript would be

var col := move_and_collide(movement_vector * delta)

if col:
    global_transform.origin -= col.travel.slide(movement_vector.normalized())

Maybe I'm just tired, but this doesn't compute.
image

If we have movement straight down into a slope (black arrow), we'll get pushed out a bit along the normal because of the margins (red arrow). What you're suggesting is that we move back the offset we traveled before colliding, projected along the vector of the direction we traveled (cyan arrow)?

So what I expect the move_and_collide to do is only move along the vector given
image
forgive the drawing
the black arrow is the desired vector to move along, the green arrow is the travel vector given in the KinematicCollision, and red is the opposite of the ideal direction to correct by. My code is an approximate solution which just slides the travel vector against the movement vector, which returns the component of the travel vector which is perpendicular to the movement vector. Subtracting this from your position should return you back onto the path you desired to travel along. Do note that this correction that is calculated is not the same as the red arrow

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment