godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
90.36k stars 21.06k forks source link

world_to_map() returns inconsistent values when using raycast #46561

Open edddieee opened 3 years ago

edddieee commented 3 years ago

Godot version:

3.2.3-stable_x11.64 (Also tested in 3.1.2-stable_x11.64)

OS/device including version:

Manjaro 20.2.1 Nibia

Issue description:

The world_to_map() sometimes return inconsistent values when using the intersect_ray() method. See the output from the image below:

1 2

Looking the z axis from output you will see an inconsistent values between the first and second image. I also tried to reproduce the world_to_map method, dividing the result.position by the cell_size and then using the floor() method, but in both cases I get the same inconsistent value.

    var space_state = get_world().direct_space_state
    var start = camera.project_ray_origin(crosshair.position)
    var end = start + camera.project_ray_normal(crosshair.position) * ray_length
        var cell_size = 2

    var result = space_state.intersect_ray(start, end, [self.get_rid()])
    if result and Input.is_action_just_pressed("ui_accept"):
        print(str(result.position) + " # WORLD SPACE")
        print(str(result.position / cell_size) + " # CELL COORDS")
        print(str((result.position / cell_size).floor()) + " # CELL COORDS with floor()s")
        print(str(grid_map.world_to_map(result.position)) + " # world_to_map()")
        print("====================")

Steps to reproduce:

Minimal reproduction project:

GridMapRaycastBug.zip

pouleyKetchoupp commented 3 years ago

The raycast hit position is not accurate enough to retrieve the corresponding cell by position, because the result is exactly at the edge between two cells, and mathematical approximations will give you inconsistent results.

There's currently no easy way to get the proper cell that was hit by the raycast though, so that needs to be fixed in the GridMap API. Since it's already possible to get the hit shape from the raycast result (it's the internal shape index from the physics server, so it's not very useful at the moment), an easy way would be to add a function to GridMap like shape_index_to_map() in order to get the map coordinates from a shape. It can be useful for collision information too.

As a workaround, in your case you can add a little bit of extra distance to your raycast hit position to make sure it's inside the box when a hit occurs, and it will give you consistent results:

var space_state = get_world().direct_space_state
    var start = camera.project_ray_origin(crosshair.position)
    var ray_dir = camera.project_ray_normal(crosshair.position)
    var end = start + ray_dir * ray_length

    var result = space_state.intersect_ray(start, end, [self.get_rid()])
    if result and Input.is_action_just_pressed("ui_accept"):
        var hit_pos = result.position
        hit_pos += ray_dir * 0.001 # add some extra distance to ensure we're inside the hit cell
        print(str(hit_pos) + " # WORLD SPACE")
        print(str(hit_pos / 2) + " # CELL COORDS")
        print(str((hit_pos / 2).floor()) + " # CELL COORDS with floor()s")
        print(str(grid_map.world_to_map(hit_pos)) + " # world_to_map()")
        print("====================")
edddieee commented 3 years ago

Hey @pouleyKetchoupp , thanks for all information! The workaround works very well!

michasng commented 11 months ago

With Godot 4.1.2 you can actually get the normal vector of the hit position to calculate which cell was hit and which cell was in front of that cell. Like this:

func _input(event: InputEvent):
    var crosshair_position = camera.get_window().size / 2 # cursor at the center, because I'm using MOUSE_MODE_CAPTURED
    var ray_start = camera.project_ray_origin(crosshair_position)
    var ray_dir = camera.project_ray_normal(crosshair_position)
    var ray_end = ray_start + ray_dir * RAY_LENGTH

    var intersect_ray_params = PhysicsRayQueryParameters3D.create(ray_start, ray_end, 0xFFFFFFFF, [get_rid()])
    var result = get_world_3d().direct_space_state.intersect_ray(intersect_ray_params)

    if not result or not is_instance_of(result.collider, GridMap):
        return

    var grid_map = (result.collider as GridMap)
    var normal_offset = grid_map.cell_size / 2 * result.normal
    # the cell that has been hit
    var cell_world_position: Vector3 = result.position - normal_offset
    var cell_position = grid_map.local_to_map(cell_world_position)
    # the "empty" cell in front of the cell being hit
    var empty_world_position: Vector3 = result.position + normal_offset
    var empty_position = grid_map.local_to_map(empty_world_position)