'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 theMeshInstanceandcull_masksof theCamerato make sure only theCamerain theViewportcan see it. - Or, add it as a child of the
Viewport, do something similar to what we did for theCamera, so it copies the position of the original, and setown_worldtotrueon theViewport, 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 |
