Search code examples
3dcollision-detectioncollisiongdscript2d-3d-conversion

Height of a slope at a point's position (3D collision)?


I'm trying my best to describe it:

I've got a 3D sloped surface, and I wanna know the height(Z) it is at any given point inside of it. I have the surface's vertices' 3D positions, and the point's 2D position. How to know what should be the height it would collide with while at the surface?Example

I'm using GDScript, making a 3D/2.5D Doom-like engine in Godot's 2D engine. Flat floors and walls work perfectly, but slopes? I've tried everything I could think of for months and nothing worked.


Solution

  • This is what you would do:

    • We define a line that is vertical and passes through your 2D point.

      Let us say our 2D point is position. Then:

      • A point on the line is position augmented with 0 on the height component. I'll call it v0:

        var v0 = Vector3(position.x, position.y, 0.0)
        
      • And the direction of the line is vertical, so zero on both 2D components, and 1 on the height component. I'll call it dir:

        var dir = Vector3(0.0, 0.0, 1.0) 
        
    • We define a plane by the three points that make up your 3D triangle.

      Let us say our 3D points are v1, v2 and v3.

      • By definition they are all on the plane.

      • We can find the normal of the plane using cross product, like this:

        var normal := (v2 - v1).cross(v3 - v1)
        

        For this use case we don't care if we got the normal flipped.

        Ah, but it will be convenient to ensure it is of unit length:

        var normal := (v2 - v1).cross(v3 - v1).normalized()
        
    • We find the interception of the line and the plane. That is our solution.

      We want to find a point on the plane, I'll call it r. That is a point such that the vector that goes from one of the points on the plane to it is perpendicular to the normal of the plane. Meaning that (r - v1).dot(normal) == 0.

      Please notice that I'm using ==, which is the equality comparison operator. I'm using that because it is an equation, not an assigment.

      The point r must also belong to the line. We can use the parametric form of the line, which is like this: r = v0 + dir * t. And we will have to first find what parameter t must be, so we can compute r.

      So we replace r here:

      (r - v1).dot(normal) == 0
      

      Which gives us this:

      ((v0 + dir * t) - v1).dot(normal) == 0
      

      Rearrange:

      (dir * t + v0 - v1).dot(normal) == 0
      

      By distributive property of the dot product:

      (dir * t).dot(normal) + (v0 - v1).dot(normal) == 0
      

      Since t is an scalar we can take it out of the dot product:

      t * dir.dot(normal) + (v0 - v1).dot(normal) == 0
      

      Subtract (v0 - v1).dot(normal) on both sides:

      t * dir.dot(normal) == - (v0 - v1).dot(normal)
      

      We can move that negative sign inside the dot product:

      t * dir.dot(normal) == (v1 - v0).dot(normal)
      

      And divide both sides by dir.dot(normal):

      t == (v1 - v0).dot(normal) / dir.dot(normal)
      

      Thus, our point r is:

      var r := v0 + dir * (v1 - v0).dot(normal) / dir.dot(normal)
      

      Then you can read the height component of r, which is your answer.

      By the way Godot has a method that is almost this. But it intersects a ray, not a line: intersect_ray of the Plane class.