'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 theMeshInstance
andcull_masks
of theCamera
to make sure only theCamera
in theViewport
can 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_world
totrue
on 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 |