'When raycasting forms multiple intersections, which point does it return? Can which point is returned be controlled?

I'm working on a chess "game" in Godot where players can import 3D models for the board and wrap a checkerboard around it with uv coordinates. All the conversions between world, local, and uv space required for this project have been crazy. But, one of the more unexpected caveats with complex board shapes, particularly my freshly imported klein bottle, is that raycasting a line from the camera will theoretically have multiple solutions, but only return one.

When the player clicks on the screen, I think raycasting should find the intersection closest to them, but Godot's PhysicsDirectSpaceState.intersect_ray() method is returning whichever intersection it finds first. This intersection could either be the correct one or not, and with up to 4 intersection points with the klein bottle, the chances are slim that the correct intersection is returned. The RayCast object does the same thing, only storing one intersection point.

Is there a way to return all the intersection points of a ray/line in Godot? That way I could sort through them to find the closest intersection point to the Player node. I also feel like this shouldn't be an issue in the first place. Godot's Ray-Casting tutorial describes the ray as "hitting something" when it returns a values, and that intuitively tells me that the closest intersection to the "ray origin" argument will be returned.

If you have the stomach for work-in-progress spaghetti code, I have a GitHub project with all the source code on it. And here is the code I use whenever I raycast in the game:

#cast out a ray from the camera, given a physics state s
static func raycast(var p:Vector2, var c:Camera, 
    var w:World, var v:float = INF, var mask:int = 0x7FFFFFFF):
    
    #get physics state of the current scene
    var s = w.direct_space_state
    #origin and normal of the camera
    var o:Vector3 = c.project_ray_origin(p)
    var n:Vector3 = c.project_ray_normal(p)
    #get ray intersection with that scene
    var r = s.intersect_ray(o, n * v, [], mask)
    #if the intersection lands, return r
    if !r.empty():
        return r
    return null

Above from BoardConverter.gd, line 472.



Solution 1:[1]

We could render to a hidden Viewport and query that.

So let us start by adding a Viewport to your scene. Make sure it has a size set. In fact, we can resize to match the main Viewport with a script. For example:

extends Viewport


func _ready() -> void:
    # warning-ignore:return_value_discarded
    get_tree().connect("screen_resized", self, "resize")


func resize() -> void:
    size = get_viewport().size

Alternatively:

extends Viewport


func _ready() -> void:
    # warning-ignore:return_value_discarded
    get_viewport().connect("size_changed", self, "resize")


func resize() -> void:
    size = get_viewport().size

Also make sure to set render_target_update_mode to UPDATE_ALWAYS. Set render_target_v_flip to true, and Set transparent_bg to true.

Then you can add a Camera child with a script that copies the global_transform of the main Camera, and make it current (under the Viewport node). For example:

extends Camera


func _ready() -> void:
    current = true


func _process(_delta: float) -> void:
    global_transform = get_tree().root.get_camera().global_transform

You might also want to copy all the relevant properties of the camera to make sure the view matches:

    var tracked_cam:Camera = get_tree().root.get_camera()
    global_transform = tracked_cam.global_transform
    fov = tracked_cam.fov
    keep_aspect = tracked_cam.keep_aspect
    cull_mask = tracked_cam.cull_mask
    environment = tracked_cam.environment
    h_offset = tracked_cam.h_offset
    v_offset = tracked_cam.v_offset
    doppler_tracking = tracked_cam.doppler_tracking
    projection = tracked_cam.projection
    fov = tracked_cam.fov
    size = tracked_cam.size
    frustum_offset = tracked_cam.frustum_offset
    near = tracked_cam.near
    far = tracked_cam.far

And what you want to render in that Viewport is your mesh, but with a different material. So you are making a duplicate of your MeshInstance, and you can either:

  • Add it as a child of the original, and use layers (under "VisualInstance" in the Inspector Panel) of the MeshInstance and cull_masks of the Camera to make sure only the Camera in the Viewport can see it.
  • Or, add it as a child of the Viewport, do something similar to what we did for the Camera, so it copies the position of the original, and set own_world to true on the Viewport, so it only renders what is inside of it.

About the material, it will be a ShaderMaterial that outputs the UV as color (write ALBEDO, and set the shader as unshaded):

shader_type spatial;
render_mode unshaded;

void fragment()
{
    ALBEDO = vec3(UV, 0.0);
}

Then in a script attached to some other Node. It will have a export var of type Texture and set it to a new ViewportTexture from the Viewport you defined it:

export var texture:Texture

We want to make sure that the Viewport is already in the scene tree when this Node enters. And it can find it. Thus, this Node should not be the Viewport, nor a parent of the Viewport. Also avoid using a child of the Viewport. Thus, we want a sibling. And a sibling that is before the Viewport in the scene tree.

Now, to read a pixel from the Texture, you can get an Image from it with get_data, then call lock on it, and then use get_pixel or get_pixelv. Don't forget to unlock it.

This code should work for both mouse or touch input:

func _input(event: InputEvent) -> void:
    if not (
        event is InputEventScreenDrag
        or event is InputEventScreenTouch
        or event is InputEventMouse
    ):
        return

    var image:Image = texture.get_data()
    image.lock()
    var color := image.get_pixelv(event.position)
    if color.a != 0.0:
        print(Vector2(color.r, color.g))

    image.unlock()

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1