Jittering when aligning to ground

Godot Version

v4.6.2.stable.mono.official [71f334935]

Question

I’m trying to align the player character to the ground, but its rotation jitters when in between two surfaces with different normals. How can I fix this?

This is the code I’m using:

public void GroundCheck()
{
        float shortestDist = Mathf.Inf;
        Vector2 shortestNormal = Vector2.Zero;
        Vector2 avgNormal = Vector2.Zero;
       
        foreach(RayCast2D ray in groundRays)
        {
            ray.ForceRaycastUpdate();
            if (ray.IsColliding())
            {
                if (Utils.AngleFromTo(ray.GetCollisionNormal(), UpDirection, true) <= FloorMaxAngle)
                {
                    avgNormal += ray.GetCollisionNormal();
                    if ((ray.GlobalPosition - ray.GetCollisionPoint()).Length() < shortestDist)
                    {
                        shortestDist = (ray.GlobalPosition - ray.GetCollisionPoint()).Length();
                        shortestNormal = ray.GetCollisionNormal();
                    }
                }
            }
        }
        
        avgNormal = avgNormal.Normalized();

        if (avgNormal != Vector2.Zero)
        {
            Vector2 groundNormal = avgNormal;
            if (onGround) velocity = Utils.CrossWithForward(groundNormal) * velocity.Length() * Mathf.Sign(velocity.Dot(RightDirection));
            else velocity -= Utils.ProjectVector(velocity, groundNormal, true);
            onGround = true;
            UpDirection = groundNormal;
            lastGroundNormal = groundNormal;

            if (shortestDist > 10f) GlobalPosition -= UpDirection * (shortestDist - 10f);
        }
        else
        {
            UpDirection = -gravityDir;
            onGround = false;
        }
}


public void AlignToUp()
{
        Vector2 upDir = UpDirection;
      
        float rotation = GlobalRotation; //check if correct
        float theta = Mathf.Wrap(Mathf.Atan2(upDir.X, -upDir.Y) - rotation, -Mathf.Pi, Mathf.Pi);

        float rotationSpeed = (isJumping || currMoveState == null) ? 9999999 : currMovePhysics.rotationSpeed;
        rotation += Mathf.Clamp(rotationSpeed * (float)GetPhysicsProcessDeltaTime(), 0, Mathf.Abs(theta)) * Mathf.Sign(theta);
        GlobalRotation = rotation;
}

I also tried using this for the GroundCheck:

public void GroundCheck()
 {
        if (IsOnFloor() && Utils.AngleFromTo(GetFloorNormal(), UpDirection, true) <= FloorMaxAngle)
        {
            //GD.Print(Mathf.RadToDeg(GetFloorAngle()));
            Vector2 groundNormal = GetFloorNormal();
            if (onGround) velocity = Utils.CrossWithForward(groundNormal) * velocity.Length() * Mathf.Sign(velocity.Dot(RightDirection));
            else velocity -= Utils.ProjectVector(velocity, groundNormal, true);
            onGround = true;
            UpDirection = groundNormal;
            lastGroundNormal = groundNormal;
        }
        else 
        {
            UpDirection = -gravityDir;
            onGround = false;
        }
}

But the jittering for this is worse.

You’re updating UpDirection from the raw normal every frame. On edges that normal flips between two surfaces, so the rotation keeps chasing it.

Lerp UpDirection toward the new normal instead of assigning it directly. Also skip the average, use the closest ray’s normal. Averaging at corners usually makes the flicker worse.

you can ease it with

UpDirection = UpDirection.Lerp(groundNormal, 1f - Mathf.Exp(-smoothSpeed * (float)GetPhysicsProcessDeltaTime())).Normalized();