godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Clarify what "global" means in the context of mouse position in different coordinate systems #3866

Open madmiraal opened 2 years ago

madmiraal commented 2 years ago

Describe the project you are working on

Godot engine

Describe the problem or limitation you are having in your project

The word "global" implies that there is one "true" coordinate system that everything else is relative to. But there are at least five coordinate systems:

And, there can be multiples of each of these.

Godot receives mouse positions in window coordinates and it needs to translate these into the appropriate position in the appropriate coordinate system. When referring to position especially global_position in properties and methods, it's often unclear which coordinate system this position is or even should be in.

When working in 3D it's easy to notice the difference between world coordinates and viewport coordinates, because one is in 3D and one is in 2D. However, until one adds a camera to a 2D game, the difference between world coordinates, canvas layer coordinates and viewport coordinates is not obvious, because they're all the same. The current best explanation for the difference between all of these in the documentation is: Viewport and canvas transforms. However, it adds to the confusion by identifying two types of global canvas transforms: viewport_transforms2

As already described in #2139, adding viewports is confusing. However, even without adding additional worlds, canvas layers, viewports or even a camera, the user is expected to understand the difference between viewport coordinates and window coordinates (which persistently remains a source of confusion see #2822, #3769 and godotengine/godot#47522).

Clarifying the difference between viewport coordinates and window coordinates is required before we can even fix issues like godotengine/godot#25023 and godotengine/godot#28665.

In addition, before we can fix issues like godotengine/godot#30950, get_mouse_position() and warp_mouse(position) need to refer to the same coordinate system, but which one? Viewport or Window?

Furthermore, when working with additional canvas layers or viewports i.e. there is a difference between the coordinate systems, even the conversion between the location of the mouse pointer and the position in the world fails (see godotengine/godot#30215 and godotengine/godot#35965).

However, before we can fix issues like godotengine/godot/issues/35965, we need to understand whether CanvasItem.get_global_mouse_position() should return the "global" mouse position in the the canvas layer or the viewport (see https://github.com/godotengine/godot/issues/35965#issuecomment-1013777262). Similarly, just as InputEventMouse position changes, not only depending on which node is interested in the event, but whether the it's received in the _input(event), _gui_event(event) or _unhandled_event(event) callback, should the global_position change to reflect the appropriate coordinate system i.e. the (root) viewport or canvas layer? Note: It currently returns the mouse's position in the window coordinates, which is practically useless.

Moreover, before we can fix issues like godotengine/godot#30215, we need to be able to answer questions like https://github.com/godotengine/godot/issues/30215#issuecomment-846606123:

I have a question regarding the behaviour of get_global_mouse_position. Suppose we have two sub-windows of the Godot Editor open at a time, say Project Settings (viewport V1) and Editor Settings (viewport V2). At any instance of time, if we call V1.get_global_mouse_position() and V2.get_global_mouse_position(). Should they give the same result (ideally)?

These questions are all pre-requisites to fixing problems like godotengine/godot#48368 and godotengine/godot#20619, which is actually a reopening of godotengine/godot#1383, which was closed by disabling the functionality in e997c0d with the comment:

it' s better to just disable selection of stuff inside a viewport, as it' s too difficult to figure out what' s going on inside.

Finally, as identified in godotengine/godot#38444, even the definition of position in Control and Node2D is inconsistent. And the difference between a global point and a local point shouldn't require the current lengthy explanations for to_global and to_local, but addressing these issues is beyond the scope of this proposal.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Ensure that anything referring to position; especially global_position uses the correct coordinate system. Just like Node2D and Node3D positions refer to their position in world coordinates, and CanvasItems' position refers to their position in CanvasLayer coordinates, any reference to position or global_position should be in the same coordinate system as the caller.

Specifically:

More generally, when providing or returning a mouse's position it should never be in window (never mind screen) coordinates. Mappings between the game world and the mouse should always be via the Viewport's coordinates.

Finally, we need to make a point of not using the word "screen" when we mean "window", or "window" when we mean "viewport". This includes doing a better job of explaining the difference between Viewport coordinates and window coordinates and how stretch modes work, because the current documentation is failing to do this (see #2479). This will be especially important for 4.0 since we now have a new Window class that derives from Viewport.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

N/A

If this enhancement will not be used often, can it be worked around with a few lines of script?

N/A

Is there a reason why this should be core and not an add-on in the asset library?

N/A

Xrayez commented 2 years ago

When I started implementing my own little engine for learning purposes, I've figured from the OpenGL perspective that model, view, and projection transformation matrices have a close relationship to the concepts of local and global coordinates. We could map how each stage of transformation corresponds to each Godot's specific transform pipeline.

Therefore, I think clarifying this somewhere in Godot's documentation could also be helpful in understanding those concepts from the graphics point of view. Except for things like normalized device coordinates which are rarely mentioned or relevant in the context of Godot, unless we talk about shaders specifically.

Sauermann commented 2 years ago

Edit: Now I understand the reasoning for the original proposal. Consider this post obsolete.

I would like to make a suggestion for the coordinates in events. In the current proposal gui_input uses different coordinate-systems than input and unhandled_input.

I believe, that a different distinction would make more sense:

In the last two cases, _gui_input is never called.

The main reason for this suggestion is:

There are nodes, for which mouse coordinates make sense to evaluate (CanvasItem and their descendants) and other nodes, for which mouse coordinates make no sense, because they do not have a position. When the evaluation of mouse coordinates makes sense, they should always be available in the same coordinate system, no matter if in input, gui_input or unhandled_input, which is easier to understand for users.

Also this would allow to repurpose code from gui_input to unhandled_input without changes and remove the requirement to recalculate the coordinates for _gui_input.

Sauermann commented 2 years ago

I created as a first attempt a graphical representation of the transform hierarchy

Transforms drawio

Sauermann commented 2 years ago

Node._input(event) and Node.unhandled_input(event) InputEventMouse.get_global_position() should return the mouse's position in the root viewport (not window coordinates).

I suggest, that we clarify, what "root viewport" means, because this could have several different interpretations:

  1. The viewport of the nearest window in the scene tree
  2. The viewport of the nearest non embedded window in the scene tree
  3. The viewport of the root node

Nr 3 doesn't make sense when dealing with non embedded windows. Not sure, which of the two other two would make more sense. I have a slight preference for nr 2.

Sauermann commented 2 years ago

I believe, that I have found a better way to illustrate the different coordinate systems, transforms and functions. Red marks the spots, where I currently see inconsistencies.

TransformsCoordinateSystems drawio

romlok commented 1 year ago

Counter-proposal: Make the caller context irrelevant; deprecate global entirely; and be explicit about the coordinate system used

Requiring devs to interpret "global" differently depending on the context (where it's being called from, and on what it's being called) is an unnecessary brainpower drain IMO. So I suggest that the functions and properties themselves described the context, eg: viewport_position, canvas_position.

This then encourages the user to be informed about the differences between the various coordinate systems, and prompts them to think more carefully about which one they actually mean to use.

It also brings the added benefit that a single class could then expose multiple such values, freeing the user from needing to manually convert (often incorrectly IME :see_no_evil:) from one coordinate system to another so often. Eg: having both Node2D.canvas_position and Node2D.viewport_position.

Further, it reduces backward-compatibility breakage, since the various global_positions can stick around, with their current inconsistent behaviour, until they can safely be removed in a subsequent major engine version.

For reference, https://github.com/godotengine/godot-proposals/issues/4250 suggests standardising get_xxx_transform() nomenclature in a similar manner, so I would say the xxx_position names should mirror those as much as possible.

Zireael07 commented 1 year ago

+100 to making caller context irrelevant. Also +100 to standardising like #4250 suggests

Sauermann commented 1 year ago

Renaming functions is considered as breaking compatibility during v4.x, so that could be done only for a future v5.

timothyqiu commented 1 year ago

I think you can still keep both versions to maintain compatibility (marking the old one as deprecated).

romlok commented 1 year ago

Renaming functions is considered as breaking compatibility during v4.x, so that could be done only for a future v5.

Yeah, I think some of the meaning in my original reply might've got lost in a pre-submit edit, but I wasn't suggesting to rename any of the existing functions, but instead add new ones with more specific names. Thus no breaking changes are needed until it's time to remove the global_ stuff, which could be done at any opportune time.

alfredomaussa commented 1 year ago

Today I face this. I used even.global_position and got the same as event.position (for mouse_event). I spent some time figuring out that the position of the viewport is different from the world.

The part most confusing was trying to use node.to_local(event.position) as it was wrong because the global position of mouse event is different from world.

rafcon-dev commented 1 year ago

I've been spending the last couple hours simply trying to drag a control with my mouse. For the life of me, it is always offset in some way. This is incredibly confusing and needs better documentation/refactoring

melek commented 9 months ago

Ran into this topic this week when trying to use touch inputs, which according to the docs are in "screen (global)" coordinates vs. the CanvasLayer global coordinates provided by get_global_mouse_position() and needed to properly position CanvasItem nodes on the layer.

I ended up having to figure out the offset with something like this:

func get_touch_pos_offset():
    # Works as long as the current scene's position is 0, 0 & a camera2d is present
    return get_viewport().get_camera_2d().get_screen_center_position() - get_viewport_rect().size / 2

func _input(event):
    var global_event_pos
    if event is InputEventScreenTouch and event.is_pressed():
        global_event_pos = event.position + get_touch_pos_offset()
    else: global_event_pos = get_global_mouse_position()

    # ...Input processing using global_event_pos ...    

Emulating mouse clicks with taps generally hides the difference by letting you work with mouse coordinates exclusively, but isn't perfect - and when working with input positions directly the coordinate confusion gets pretty frustrating.

Glad I found this issue before I misdiagnosed this as a bug and filed a new issue; I think this boils down to an eccentricity of Godot coordinate systems and when which ones are applied.