Hello! I'm currently working on a game where the player (CharacterBody3D) is often on ramps. I'm aligning the player to the surface using raycasts and the returned normals (the barymetric way). Here's my problem:

This is what I want (green line) vs. what is actually happening (red line) - excuse the crude drawing:

As I drew in the close up, the raycast hits the edge, which then triggers to player to snap to that collision point. What I want to do is prevent the player from snapping to the corner (red arrow), and instead continue upwards (green arrow).

I attempted to use angle_to() between the current normal at the player's position and the collision point. I set the condition to if the angle is >=90 degrees, do not perform the surface alignment - but I'm getting inconsistent results depending on the surface itself (primitive vs array mesh geometry). I'd also like to have a bit more flexibility modifying this behavior based on the player's speed, etc.

Put simply - I am looking for a way to detect when the player is about to hit a sharp edge.

Are there better/more flexible ways to accomplish this than using angle_to()? Specifically, should I use other colliders/raycasts to detect this?

  • xyz replied to this.
  • paftdunk I think you first need to decide how exactly will movement be working. Without that it's hard to give useful suggestions.

    But to answer directly what you asked; try to raycast from the point that's slightly offset in skate's forward direction and goes into skate's down direction. Limit the length of the ray. If the ray doesn't hit anything or if a signed angle between the current normal and the rayhit normal is larger than some threshold - you have a "sharp" convex edge ahead.

    The vertex normals do look kinda strange. Looks like your hard edges are not truly hard 🙂

    paftdunk Implement jumping first.
    Are your meshes correctly representing hard edges in their normals?

      xyz Hey again @xyz!

      Good point - I've implemented rudimentary jumping prior to this, but I am going to refine it before proceeding.

      From what I can tell, the normals are looking correct - but I could be wrong. Do you agree?

      • xyz replied to this.

        paftdunk I think you first need to decide how exactly will movement be working. Without that it's hard to give useful suggestions.

        But to answer directly what you asked; try to raycast from the point that's slightly offset in skate's forward direction and goes into skate's down direction. Limit the length of the ray. If the ray doesn't hit anything or if a signed angle between the current normal and the rayhit normal is larger than some threshold - you have a "sharp" convex edge ahead.

        The vertex normals do look kinda strange. Looks like your hard edges are not truly hard 🙂

          paftdunk You want this:

          If you used Blender's Smooth By Angle modifier be sure to enable On Cage preview for the modifier, or apply it. Otherwise, you won't see the actual result in the normals preview. Also if you don't apply it manually, you need to tell the exporter to do so.

            xyz Right now, the movement is the basic approach to CharacterBody3D movement. Apply velocity on z basis based on forward/back input, rotate player by constructing a new transform based on left/right input. I apply gravity to the velocity each frame.

            When, and only when, the player is grounded, do I snap to the surface normals. Also, the surface alignment snap does not affect the y-axis (no offset, etc.). So I perform the jump by adding a force to the velocity - as soon as is_on_ground() returns false, the snapping stops and gravity takes care of the rest. Snapping resumes as soon as is_on_ground() returns true.

            That is what I have right now; however, I can see how the combination of the normal snapping and gravity (added on to the CharacterBody's sliding behavior) may add some complexity to the ramp behavior.

            xyz Ah - much better 🙂

            • xyz replied to this.

              paftdunk Relying on is_on_ground() in this scenario may not be the best approach because character body will lose on ground status as soon as the collider normal diverges from up_direction by floor_max_angle.

                xyz I've noticed as I've experimented with rounder objects this definitely seems to be the case. I think what I'll do is update the up_direction to be the interpolated normal, but through other means decide what angles we should stop snapping to.

                xyz I do have a question about this:

                If the ray doesn't hit anything or if a signed angle between the current normal and the rayhit normal is larger than some threshold - you have a "sharp" convex edge ahead.

                When I hit this condition - the sharp edge being detected - I'm not sure what course of action to take. Here's an example:

                Say the RED arrow is the raycast that is used for normal calculations for alignment. Say the PURPLE arrow is the raycast that checks ahead. The GREEN line is a very tiny face on the mesh right before hitting the hard edge.

                If the PURPLE arrow detects the hard edge, and then I disable floor alignment at that point, the RED arrow still hasn't hit that last segment (the green one). So, the player will continue to move in their respective forward direction without having aligned to the green segment because I turned it off too early; it'd still be aligned with the normal that it hit right before I detected the edge (technically not the one I want it to be aligned with).

                Does that make sense? Would I simply just need to move the forward-checking raycast back, closer to the raycast that is used for floor alignment?

                • xyz replied to this.

                  paftdunk If the green distance is very small it may be good enough as the difference between the actual surface tangent and the current forward would be negligible.

                  If you want the exact tangent then take the face normal at the red position (returned by red raycast), and rebuild the basis using it and the current forward direction in the first cross product.

                    xyz Excellent, this looks good already! I'm using that method for now rather than checking the angle. I'm also going to make it a "rule" for when I'm making my meshes to not make any faces that may cause that scenario I described - shouldn't be hard to avoid with the style I'm going for.

                    Related - I'm running into a slight issue (well, kinda) with the velocity. This is what I want to create:

                    However, because the last normal isn't exactly 90 degrees, there is a bit of velocity on the the x-axis. This causes the character body to overshoot the ramp and end up going behind it (on the x-axis).

                    This makes logical sense of course, this ramp isn't launching me exactly perpendicular to the ground so I'd still have velocity on all axes. IDEALLY, they'd just continue to move up and come right back down to slide down the ramp. So I'd only want velocity on the y and z axes even though this isn't realistic (I'm going for stylized movement after all).

                    Here's an example from THPS. I don't think it purely uses rigidbody simulation - there's definitely a lot of kinematic movement from testing I've done, which I why I use this as a reference:

                    You can see that the ramp is not perfectly 90 degrees at the point at which the skater leaves.

                    I was thinking I could lock the character body into some sort of spline/curve that would keep this from happening, but somehow keep the gravity in control for the y-axis and allow movement on the z-axis. Or I could just lock x-velocity while they are in the air and turn it back on once they land, but this feels like a hacky solution that may not be ideal in all situations, so I'm not sure that's a good path. What would you say would be a good approach to this?

                    • xyz replied to this.

                      paftdunk Check if the horizontal component of the velocity at the time of detachment from the ramp is below some threshold. If yes, set it to zero, keeping only the vertical component. You'll have to experiment a bit to determine the exact threshold. It may also depend on game mechanics and movement stylization you're after.

                      Alternatively (or additionally) you may gradually push the skater away from the ramp (in the last attached normal direction) for some brief time following the detachment.

                      To get the horizontal velocity component, project the velocity vector onto the horizontal plane and take its length.

                        xyz Tried both and it's great for now! I might have to reevaluate how I'll handle the physics for that as I continue to add mechanics - but the sharp edge detection was the biggest piece that'll stay the way it is.

                        Thank you!